Ajout du support des groupes dans la modification d'une recette.

This commit is contained in:
FyloZ 2021-03-26 15:48:18 -04:00
parent 39591905f2
commit 48e900f3c5
17 changed files with 256 additions and 49 deletions

View File

@ -18,6 +18,7 @@ import {MixAddComponent} from './pages/mix/mix-add/mix-add.component';
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component';
import { ImagesEditorComponent } from './components/images-editor/images-editor.component';
import { MixesCardComponent } from './components/mixes-card/mixes-card.component';
import {MatSortModule} from '@angular/material/sort'
@NgModule({
@ -29,7 +30,8 @@ import { MixesCardComponent } from './components/mixes-card/mixes-card.component
ColorsRoutingModule,
SharedModule,
MatExpansionModule,
FormsModule
FormsModule,
MatSortModule
]
})
export class ColorsModule {

View File

@ -1,16 +1,63 @@
<mat-expansion-panel class="table-title" [expanded]="true" [disabled]="true">
<mat-expansion-panel-header>
<mat-panel-title>Étapes</mat-panel-title>
</mat-expansion-panel-header>
<mat-card>
<mat-card-header>
<mat-card-title>Étapes</mat-card-title>
</mat-card-header>
<mat-card-content class="no-action">
<mat-form-field>
<mat-label>Groupe</mat-label>
<mat-select [(ngModel)]="selectedGroupId">
<mat-option *ngFor="let group of (groups$ | async)" [value]="group.id">
{{group.name}}
</mat-option>
</mat-select>
</mat-form-field>
<table #stepTable mat-table [dataSource]="steps">
<p *ngIf="!selectedGroupId" class="empty-text text-center">Aucun groupe n'est sélectionné</p>
<ng-container
*ngIf="selectedGroupId"
[ngTemplateOutlet]="stepTableTemplate">
</ng-container>
</mat-card-content>
</mat-card>
<ng-template
#stepTableTemplate>
<table
#matTable
mat-table
matSort
[dataSource]="selectedGroupSteps">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let step; let i = index">{{i + 1}}</td>
<td mat-cell *matCellDef="let step">{{step.position}}</td>
</ng-container>
<ng-container matColumnDef="buttonsPosition">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let step; let i = index">
<ng-container *ngIf="(!hoveredStep && i === 0) || hoveredStep === step">
<button
mat-mini-fab
color="primary"
class="mr-1"
[disabled]="step.position <= 1"
(click)="decreasePosition(step, matTable)">
<mat-icon svgIcon="arrow-up"></mat-icon>
</button>
<button
mat-mini-fab
color="primary"
[disabled]="step.position >= selectedGroupStepsCount"
(click)="increasePosition(step, matTable)">
<mat-icon svgIcon="arrow-down"></mat-icon>
</button>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="message">
<th mat-header-cell *matHeaderCellDef>Message</th>
<th mat-header-cell *matHeaderCellDef>Contenu</th>
<td mat-cell *matCellDef="let step">
<mat-form-field>
<input matInput type="text" [(ngModel)]="step.message"/>
@ -20,11 +67,11 @@
<ng-container matColumnDef="buttonRemove">
<th mat-header-cell *matHeaderCellDef>
<button mat-raised-button color="accent" (click)="addStep()">Ajouter</button>
<button mat-raised-button color="accent" (click)="addStep(matTable)">Ajouter</button>
</th>
<td mat-cell *matCellDef="let step; let i = index">
<ng-container *ngIf="(!hoveredStep && i === 0) || hoveredStep === step">
<button mat-raised-button color="warn" (click)="removeStep(i)">Retirer</button>
<button mat-raised-button color="warn" (click)="removeStep(i, matTable)">Retirer</button>
</ng-container>
</td>
</ng-container>
@ -32,4 +79,4 @@
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let step; columns: columns" (mouseover)="hoveredStep = step"></tr>
</table>
</mat-expansion-panel>
</ng-template>

View File

@ -1,5 +1,8 @@
mat-expansion-panel
min-width: 560px
.empty-text
color: rgba(0, 0, 0, 0.54)
mat-form-field
width: 20rem

View File

@ -1,6 +1,8 @@
import {Component, Input, ViewChild} from '@angular/core';
import {RecipeStep} from "../../../shared/model/recipe.model";
import {MatTable} from "@angular/material/table";
import {Component, Input} from '@angular/core'
import {Recipe, RecipeStep, recipeStepsForGroupId, sortRecipeSteps} from '../../../shared/model/recipe.model'
import {MatTable} from '@angular/material/table'
import {Observable} from 'rxjs'
import {EmployeeGroup} from '../../../shared/model/employee'
@Component({
selector: 'cre-step-table',
@ -8,20 +10,89 @@ import {MatTable} from "@angular/material/table";
styleUrls: ['./step-table.component.sass']
})
export class StepTableComponent {
@ViewChild('stepTable', {static: true}) stepTable: MatTable<RecipeStep>
readonly columns = ['position', 'message', 'buttonRemove']
readonly columns = ['position', 'buttonsPosition', 'message', 'buttonRemove']
@Input() steps: RecipeStep[]
@Input() recipe: Recipe
@Input() groups$: Observable<EmployeeGroup[]>
@Input() selectedGroupId: number | null
hoveredStep : RecipeStep | null
hoveredStep: RecipeStep | null
addStep() {
this.steps.push({id: null, message: ""})
this.stepTable.renderRows()
private groupSteps = new Map<number, RecipeStep[]>()
addStep(table: MatTable<any>) {
const addedStep = new RecipeStep(null, '', this.selectedGroupSteps.length + 1)
this.selectedGroupSteps.push(addedStep)
table.renderRows()
}
removeStep(position: number) {
this.steps.splice(position, 1)
this.stepTable.renderRows()
removeStep(position: number, table: MatTable<any>) {
this.selectedGroupSteps.splice(position, 1)
// Decreases the position of each step above the removed one
for (let i = position; i < this.selectedGroupSteps.length; i++) {
this.selectedGroupSteps[i].position -= 1
}
table.renderRows()
}
increasePosition(step: RecipeStep, table: MatTable<any>) {
this.updateStepPosition(step, step.position + 1)
this.sort(table)
}
decreasePosition(step: RecipeStep, table: MatTable<any>) {
this.updateStepPosition(step, step.position - 1)
this.sort(table)
}
sort(table: MatTable<any>) {
this.groupSteps.set(this.selectedGroupId, sortRecipeSteps(this.selectedGroupSteps))
table.renderRows()
}
selectedGroupStepAtPosition(position: number): RecipeStep {
return this.selectedGroupSteps.find(s => s.position === position)
}
selectedGroupHasStepAtPosition(position: number): boolean {
return this.selectedGroupStepAtPosition(position) != undefined
}
get selectedGroupSteps(): RecipeStep[] {
if (!this.groupSteps.has(this.selectedGroupId)) {
this.groupSteps.set(this.selectedGroupId, recipeStepsForGroupId(this.recipe, this.selectedGroupId))
}
return this.groupSteps.get(this.selectedGroupId)
}
get selectedGroupStepsCount(): number {
return this.selectedGroupSteps.length
}
get mappedUpdatedSteps(): Map<number, RecipeStep[]> {
const updatedStepsMap = new Map<number, RecipeStep[]>()
this.recipe.groupsInformation.forEach(i => {
updatedStepsMap.set(i.group.id, i.steps)
})
// Add steps for groups that were not already in the recipe
this.groupSteps.forEach((steps, groupId) => {
if (!updatedStepsMap.has(groupId)) {
updatedStepsMap.set(groupId, steps)
}
})
return updatedStepsMap
}
private updateStepPosition(step: RecipeStep, updatedPosition: number) {
if (!this.selectedGroupHasStepAtPosition(updatedPosition)) {
step.position = updatedPosition
} else {
const conflictingStep = this.selectedGroupStepAtPosition(updatedPosition)
conflictingStep.position = step.position
step.position = updatedPosition
}
}
}

View File

@ -3,7 +3,9 @@
<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="accent" (click)="submit(editComponent)" [disabled]="editComponent.form && editComponent.form.invalid">Enregistrer</button>
<button mat-raised-button color="accent" (click)="submit(editComponent, stepTable)"
[disabled]="editComponent.form && editComponent.form.invalid">Enregistrer
</button>
<button mat-raised-button color="warn" *ngIf="hasDeletePermission" (click)="delete()">Supprimer</button>
</div>
<mat-form-field>
@ -36,7 +38,12 @@
</div>
<div>
<cre-step-table [steps]="recipe.steps"></cre-step-table>
<cre-step-table
#stepTable
[recipe]="recipe"
[groups$]="groups$"
[selectedGroupId]="loggedInEmployeeGroupId">
</cre-step-table>
</div>
<div>

View File

@ -1,6 +1,6 @@
import {Component, ViewChild} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {Recipe} from '../../../shared/model/recipe.model'
import {Recipe, RecipeStep} from '../../../shared/model/recipe.model'
import {RecipeService} from '../../services/recipe.service'
import {ActivatedRoute, Router} from '@angular/router'
import {Validators} from '@angular/forms'
@ -12,6 +12,9 @@ import {EntityEditComponent} from '../../../shared/components/entity-edit/entity
import {ImagesEditorComponent} from '../../components/images-editor/images-editor.component'
import {ErrorModel, ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
import {GroupService} from '../../../groups/services/group.service'
import {AppState} from '../../../shared/app-state'
import {StepTableComponent} from '../../components/step-table/step-table.component'
@Component({
selector: 'cre-edit',
@ -24,6 +27,7 @@ export class EditComponent extends ErrorHandlingComponent {
@ViewChild('imagesEditor') imagesEditor: ImagesEditorComponent
recipe: Recipe | null
groups$ = this.groupService.all
formFields = [
{
name: 'name',
@ -108,8 +112,10 @@ export class EditComponent extends ErrorHandlingComponent {
constructor(
private recipeService: RecipeService,
private groupService: GroupService,
private accountService: AccountService,
private alertService: AlertService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
@ -125,10 +131,10 @@ export class EditComponent extends ErrorHandlingComponent {
parseInt(this.activatedRoute.snapshot.paramMap.get('id')),
recipe => {
this.recipe = recipe
if (this.recipe.mixes.length == 0) {
if (this.recipe.mixCount == 0) {
this.alertService.pushWarning('Il n\'y a aucun mélange dans cette recette')
}
if (this.recipe.steps.length == 0) {
if (this.recipe.stepCount == 0) {
this.alertService.pushWarning('Il n\'y a aucune étape dans cette recette')
}
},
@ -140,11 +146,18 @@ export class EditComponent extends ErrorHandlingComponent {
this.units$.next(unit)
}
submit(editComponent: EntityEditComponent) {
submit(editComponent: EntityEditComponent, stepTable: StepTableComponent) {
const values = editComponent.values
this.submittedValues = values
const steps = stepTable.mappedUpdatedSteps
if (!this.stepsPositionsAreValid(steps)) {
this.alertService.pushError('Les étapes ne peuvent pas avoir une position inférieure à 1')
return
}
this.subscribeAndNavigate(
this.recipeService.update(this.recipe.id, values.name, values.description, values.color, values.gloss, values.sample, values.approbationDate, values.remark, this.recipe.steps),
this.recipeService.update(this.recipe.id, values.name, values.description, values.color, values.gloss, values.sample, values.approbationDate, values.remark, steps),
'/color/list'
)
}
@ -159,4 +172,19 @@ export class EditComponent extends ErrorHandlingComponent {
get hasDeletePermission(): boolean {
return this.accountService.hasPermission(EmployeePermission.REMOVE_RECIPE)
}
get loggedInEmployeeGroupId(): number {
return this.appState.authenticatedEmployee.group?.id
}
private stepsPositionsAreValid(steps: Map<number, RecipeStep[]>): boolean {
let valid = true
steps.forEach((steps, _) => {
if (steps.find(s => s.position === 0)) {
valid = false
return
}
})
return valid
}
}

View File

@ -35,9 +35,9 @@
</div>
<!-- Steps -->
<div *ngIf="recipe.steps.length > 0">
<cre-step-list [steps]="recipe.steps"></cre-step-list>
</div>
<!-- <div *ngIf="recipe.steps.length > 0">-->
<!-- <cre-step-list [steps]="recipe.steps"></cre-step-list>-->
<!-- </div>-->
<!-- Images -->
<div>

View File

@ -55,7 +55,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
this.recipe = r
this.note = r.note
if (this.recipe.mixes.length <= 0 || this.recipe.steps.length <= 0) {
if (this.recipe.mixCount <= 0 || this.recipe.stepCount <= 0) {
this.alertService.pushWarning('Cette recette n\'est pas complète')
}
},

View File

@ -3,7 +3,6 @@ import {ApiService} from '../../shared/service/api.service'
import {Observable} from 'rxjs'
import {Recipe, RecipeStep} from '../../shared/model/recipe.model'
import {map} from 'rxjs/operators'
import {MaterialQuantity} from '../../material/service/inventory.service'
@Injectable({
providedIn: 'root'
@ -44,12 +43,20 @@ export class RecipeService {
return this.api.post<Recipe>('/recipe', body)
}
update(id: number, name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, steps: RecipeStep[] = null) {
const body = {id, name, description, color, gloss, sample, remark, steps}
update(id: number, name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, steps: Map<number, RecipeStep[]>) {
const body = {id, name, description, color, gloss, sample, remark, steps: []}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
}
steps.forEach((groupSteps, groupId) => {
const mappedGroupSteps = groupSteps.map(s => {
return {message: s.message, position: s.position}
})
body.steps.push({groupId, steps: mappedGroupSteps})
})
return this.api.put<Recipe>('/recipe', body)
}

View File

@ -66,7 +66,7 @@ export class AddComponent extends ErrorHandlingComponent {
icon: 'account-multiple',
type: 'select',
defaultValue: -1,
options$: this.groupService.all.pipe(map(groups => groups.map(g => {
options$: this.groupService.allWithDefault.pipe(map(groups => groups.map(g => {
return {value: g.id, label: g.name}
})))
}, {

View File

@ -49,7 +49,7 @@ export class EditComponent extends ErrorHandlingComponent {
icon: 'account-multiple',
type: 'select',
valueFn: employee => employee.group ? employee.group.id : -1,
options$: this.groupService.all.pipe(map(groups => groups.map(g => {
options$: this.groupService.allWithDefault.pipe(map(groups => groups.map(g => {
return {value: g.id, label: g.name}
})))
}, {

View File

@ -1,7 +1,6 @@
import {Component} from '@angular/core'
import {GroupService} from '../../services/group.service'
import {EmployeeGroup, EmployeePermission} from '../../../shared/model/employee'
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'
@ -14,7 +13,7 @@ import {AlertService} from '../../../shared/service/alert.service'
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
groups$ = this.groupService.all.pipe(map(groups => groups.filter(g => g.id >= 0)))
groups$ = this.groupService.all
defaultGroup: EmployeeGroup = null
columns = [
{def: 'name', title: 'Nom', valueFn: g => g.name},

View File

@ -15,6 +15,10 @@ export class GroupService {
get all(): Observable<EmployeeGroup[]> {
return this.api.get<EmployeeGroup[]>('/employee/group')
}
get allWithDefault(): Observable<EmployeeGroup[]> {
return this.all
.pipe(tap(groups => groups.unshift({
id: -1,
name: 'Aucun',

View File

@ -17,6 +17,7 @@ export class PermissionsListComponent {
}
get permissions(): EmployeePermission[] {
// @ts-ignore
return this.filterPermissions(this.employee ? this.employee.permissions : this.group.permissions)
}

View File

@ -1,6 +1,7 @@
import {Material} from "./material.model";
import {LocalDate} from "js-joda";
import {Company} from "./company.model";
import {Material} from './material.model'
import {LocalDate} from 'js-joda'
import {Company} from './company.model'
import {EmployeeGroup} from './employee'
export class Recipe {
constructor(
@ -12,9 +13,26 @@ export class Recipe {
public sample: number,
public approbationDate: LocalDate,
public remark: string,
public note: string,
public company: Company,
public mixes: Mix[],
public groupsInformation: RecipeGroupInformation[]
) {
}
get mixCount(): number {
return this.mixes.length
}
get stepCount(): number {
return this.groupsInformation.flatMap(i => i.steps).length
}
}
export class RecipeGroupInformation {
constructor(
public id: number,
public group: EmployeeGroup,
public note: string,
public steps: RecipeStep[]
) {
}
@ -34,7 +52,8 @@ export class MixMaterial {
constructor(
public id: number,
public material: Material,
public quantity: number
public quantity: number,
public position: number
) {
}
}
@ -51,7 +70,17 @@ class MixType {
export class RecipeStep {
constructor(
public id: number,
public message: string
public message: string,
public position: number
) {
}
}
export function recipeStepsForGroupId(recipe: Recipe, groupId: number): RecipeStep[] {
const groupInformation = recipe.groupsInformation.find(i => i.group.id === groupId)
return groupInformation ? sortRecipeSteps(groupInformation.steps) : []
}
export function sortRecipeSteps(steps: RecipeStep[]): RecipeStep[] {
return steps.sort((a, b) => a.position - b.position)
}

View File

@ -85,6 +85,15 @@ mat-expansion-panel.table-title
&:hover, &:focus
background-color: $color-primary !important
&.header-field .mat-form-field-flex
background-color: white
.mat-form-field-outline
opacity: 1
&:first-child
opacity: 0
mat-panel-title
color: $light-primary-text !important
font-weight: bold

View File

@ -12,7 +12,7 @@
"importHelpers": true,
"target": "es2015",
"lib": [
"es2018",
"es2019",
"dom"
]
},