Working mix editor

This commit is contained in:
FyloZ 2021-11-17 19:23:13 -05:00
parent c3122c2e3e
commit 971e3dcb3c
Signed by: william
GPG Key ID: 835378AE9AF4AE97
10 changed files with 237 additions and 117 deletions

View File

@ -13,7 +13,7 @@ services:
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database:3307/cre"
cre_database_url: "mysql://database/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 1

View File

@ -1,6 +1,6 @@
<mat-form-field [class.short]="short">
<mat-label *ngIf="showLabel">Unités</mat-label>
<mat-select [value]="unit" (selectionChange)="unitChange.emit($event.value)">
<mat-select [value]="unit" (selectionChange)="onUnitChange($event.value)">
<ng-container *ngIf="!short">
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>

View File

@ -1,17 +1,28 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
import {FormControl} from '@angular/forms'
@Component({
selector: 'cre-unit-selector',
templateUrl: './unit-selector.component.html',
styleUrls: ['./unit-selector.component.sass']
})
export class UnitSelectorComponent {
export class UnitSelectorComponent implements OnInit {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
@Input() unit = UNIT_MILLILITER
@Input() showLabel = true
@Input() short = false
@Input() control: FormControl | null
@Output() unitChange = new EventEmitter<string>()
ngOnInit() {
this.control?.setValue(this.unit)
}
onUnitChange(newUnit: string) {
this.control?.setValue(newUnit)
this.unitChange.emit(newUnit)
}
}

View File

@ -1,6 +1,17 @@
<ng-container *ngIf="recipe">
<cre-action-bar>
<cre-action-group>
<cre-primary-button>Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-submit-button [valid]="form.valid">Enregistrer</cre-submit-button>
</cre-action-group>
</cre-action-bar>
<cre-mix-form
*ngIf="recipe"
#form
[recipe]="recipe"
[materialTypes]="materialTypes$"
[materials]="materials$">
</cre-mix-form>
</ng-container>

View File

@ -1,4 +1,11 @@
<cre-table class="mx-auto mt-5" [dataSource]="mixMaterials" [columns]="columns">
<div class="mt-5">
<cre-warning-alert *ngIf="materialCount <= 0">
<p>Il n'y a actuellement aucun produit enregistré dans le système.</p>
<p *ngIf="hasMaterialEditPermission">Vous pouvez en créer un <b><a routerLink="/catalog/material/add">ici</a></b>.
</p>
</cre-warning-alert>
<cre-table [hidden]="materialCount <= 0" class="mx-auto" [dataSource]="mixMaterials" [columns]="columns">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.position + 1}}</td>
@ -33,12 +40,31 @@
</td>
</ng-container>
<ng-container matColumnDef="units">
<th mat-header-cell *matHeaderCellDef>Unités</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-unit-selector
*ngIf="!areUnitsPercents(mixMaterial)"
[showLabel]="false"
[short]="true"
[control]="getControls(mixMaterial.position).units">
</cre-unit-selector>
<ng-container *ngIf="areUnitsPercents(mixMaterial)">
%
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="endButton">
<th mat-header-cell *matHeaderCellDef>
<cre-accent-button (click)="addRow()" [disabled]="materialCount - mixMaterials.length <= 0">Ajouter</cre-accent-button>
<cre-accent-button (click)="addRow()" [disabled]="materialCount - mixMaterials.length <= 0">Ajouter
</cre-accent-button>
</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-warn-button (click)="removeRow(mixMaterial)" [disabled]="mixMaterials.length === 1">Retirer</cre-warn-button>
<cre-warn-button (click)="removeRow(mixMaterial)" [disabled]="mixMaterials.length === 1">Retirer
</cre-warn-button>
</td>
</ng-container>
</cre-table>
</div>

View File

@ -1,11 +1,13 @@
import {AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, ViewChild} from "@angular/core";
import {CreTable} from "../../shared/components/tables/tables";
import {MixMaterialDto, sortMixMaterialsDto} from "../../shared/model/recipe.model";
import {Observable, Subject} from "rxjs";
import {Material} from "../../shared/model/material.model";
import {FormControl, Validators} from "@angular/forms";
import {takeUntil} from "rxjs/operators";
import {CreInputEntry} from "../../shared/components/inputs/inputs";
import {AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, ViewChild, ViewChildren} from '@angular/core'
import {CreTable} from '../../shared/components/tables/tables'
import {MixMaterialDto, sortMixMaterialsDto} from '../../shared/model/recipe.model'
import {Observable, Subject} from 'rxjs'
import {Material, materialComparator} from '../../shared/model/material.model'
import {FormControl, Validators} from '@angular/forms'
import {takeUntil} from 'rxjs/operators'
import {CreComboBoxComponent, CreInputEntry} from '../../shared/components/inputs/inputs'
import {AccountService} from '../../accounts/services/account.service'
import {Permission} from '../../shared/model/user'
@Component({
selector: 'cre-mix-materials-form',
@ -13,11 +15,12 @@ import {CreInputEntry} from "../../shared/components/inputs/inputs";
})
export class MixMaterialsForm implements AfterViewInit, OnDestroy {
@ViewChild(CreTable) table: CreTable<MixMaterialDto>
@ViewChildren(CreComboBoxComponent) materialComboBoxes: CreComboBoxComponent[]
@Input() materials: Observable<Material[]>
@Input() mixMaterials: MixMaterialDto[] = []
mixMaterials: MixMaterialDto[] = []
columns = ['position', 'positionButtons', 'material', 'quantity', 'endButton']
columns = ['position', 'positionButtons', 'material', 'quantity', 'units', 'endButton']
private _allMaterials: Material[]
private _controls: ControlsByPosition[] = []
@ -25,6 +28,7 @@ export class MixMaterialsForm implements AfterViewInit, OnDestroy {
private _destroy$ = new Subject<boolean>()
constructor(
private accountService: AccountService,
private cdRef: ChangeDetectorRef
) {
}
@ -46,18 +50,21 @@ export class MixMaterialsForm implements AfterViewInit, OnDestroy {
}
addRow() {
const position = this.nextPosition;
const mixMaterial = new MixMaterialDto(0, 0, false, position);
const position = this.nextPosition
const mixMaterial = new MixMaterialDto(null, 0, false, position)
const materialIdControl = new FormControl(null, Validators.required)
const quantityControl = new FormControl(null, Validators.required)
const quantityControl = new FormControl(0, Validators.required)
const unitsControl = new FormControl(null, Validators.required)
materialIdControl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe({
next: materialId => {
this.mixMaterials.filter(x => x.materialId === mixMaterial.materialId)[0].materialId = materialId
mixMaterial.materialId = materialId
this.refreshAvailableMaterials()
this.cdRef.detectChanges()
this.materialComboBoxes.forEach(comboBox => comboBox.reloadEntries())
}
})
@ -65,7 +72,8 @@ export class MixMaterialsForm implements AfterViewInit, OnDestroy {
position,
controls: {
materialId: materialIdControl,
quantity: quantityControl
quantity: quantityControl,
units: unitsControl
}
})
this._availableMaterialsEntries.push({
@ -86,15 +94,18 @@ export class MixMaterialsForm implements AfterViewInit, OnDestroy {
updatePosition(mixMaterial: MixMaterialDto, newPosition: number, switchPositions = true) {
const currentPosition = mixMaterial.position
const currentControls = this.getControlsByPosition(currentPosition)
const currentMaterialEntries = this.getMaterialEntriesByPosition(currentPosition)
this.getControlsByPosition(currentPosition).position = newPosition
this.getMaterialEntriesByPosition(currentPosition).position = newPosition
// Update before current to prevent position conflicts
if (switchPositions) {
this.updatePosition(this.getMixMaterialByPosition(newPosition), currentPosition, false)
}
mixMaterial.position = newPosition
currentControls.position = newPosition
currentMaterialEntries.position = newPosition
this.sortTable()
}
@ -106,8 +117,24 @@ export class MixMaterialsForm implements AfterViewInit, OnDestroy {
return this.getMaterialEntriesByPosition(position).entries
}
areUnitsPercents(mixMaterial: MixMaterialDto): boolean {
return mixMaterial.materialId ? this._allMaterials.filter(x => x.id === mixMaterial.materialId)[0].materialType.usePercentages : false
}
get hasMaterialEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
}
get materialCount(): number {
return this._allMaterials ? this._allMaterials.length : 0;
return this._allMaterials ? this._allMaterials.length : 0
}
get valid(): boolean {
return this._controls
.map(controls => controls.controls)
.map(controls => [controls.materialId, controls.quantity])
.flatMap(controls => controls)
.every(control => control.valid)
}
private get nextPosition(): number {
@ -141,14 +168,27 @@ export class MixMaterialsForm implements AfterViewInit, OnDestroy {
private filterMaterials(mixMaterial: MixMaterialDto): CreInputEntry[] {
return this._allMaterials
.filter(material => mixMaterial.materialId === material.id || this.mixMaterials.filter(mm => mm.materialId === material.id).length === 0)
.map(material => new CreInputEntry(material.id, material.name))
.filter(material => {
// Prevent use of percents in first position
if (material.materialType.usePercentages && mixMaterial.position === 0) {
return false
}
if (mixMaterial.materialId === material.id) {
return true
}
return this.mixMaterials.filter(x => x.materialId === material.id).length <= 0
})
.sort(materialComparator)
.map(material => new CreInputEntry(material.id, material.name, material.materialType.prefix ? `[${material.materialType.prefix}] ${material.name}` : material.name))
}
}
interface MixMaterialControls {
materialId: FormControl
quantity: FormControl
units: FormControl
}
interface ControlsByPosition {

View File

@ -1,19 +1,19 @@
import {AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild} from "@angular/core";
import {SubscribingComponent} from "../../shared/components/subscribing.component";
import {MixMaterialDto, Recipe, sortMixMaterialsDto} from "../../shared/model/recipe.model";
import {ErrorService} from "../../shared/service/error.service";
import {ActivatedRoute, Router} from "@angular/router";
import {RecipeService} from "../services/recipe.service";
import {FormControl, Validators} from "@angular/forms";
import {Observable, Subject} from "rxjs";
import {MaterialType} from "../../shared/model/materialtype.model";
import {MaterialTypeService} from "../../material-type/service/material-type.service";
import {CreInputEntry} from "../../shared/components/inputs/inputs";
import {filter, map, takeUntil, tap} from "rxjs/operators";
import {Material} from "../../shared/model/material.model";
import {MaterialService} from "../../material/service/material.service";
import {CreTable} from "../../shared/components/tables/tables";
import {MatTable} from "@angular/material/table";
import {Component, Input, OnInit, ViewChild} from '@angular/core'
import {SubscribingComponent} from '../../shared/components/subscribing.component'
import {Recipe} from '../../shared/model/recipe.model'
import {ErrorService} from '../../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {RecipeService} from '../services/recipe.service'
import {FormControl, Validators} from '@angular/forms'
import {Observable} from 'rxjs'
import {MaterialType} from '../../shared/model/materialtype.model'
import {MaterialTypeService} from '../../material-type/service/material-type.service'
import {CreInputEntry} from '../../shared/components/inputs/inputs'
import {map} from 'rxjs/operators'
import {Material} from '../../shared/model/material.model'
import {MaterialService} from '../../material/service/material.service'
import {CreForm} from '../../shared/components/forms/forms'
import {MixMaterialsForm} from './materials-form'
@Component({
selector: 'cre-mix-add',
@ -50,30 +50,22 @@ export class MixAdd extends SubscribingComponent {
}
set recipe(recipe: Recipe) {
this._recipe = recipe;
this._recipe = recipe
this.materials$ = this.materialService.getAllForMixCreation(recipe.id)
}
get recipe(): Recipe {
return this._recipe;
return this._recipe
}
}
@Component({
selector: 'cre-mix-form',
templateUrl: 'form.html'
})
export class MixForm {
@Input() recipe: Recipe
@Input() materialTypes: Observable<MaterialType[]>
@Input() materials: Observable<Material[]>
}
@Component({
selector: 'cre-mix-info-form',
templateUrl: 'info-form.html'
})
export class MixInfoForm implements OnInit {
@ViewChild(CreForm) form: CreForm
@Input() recipe: Recipe
@Input() materialTypes: Observable<MaterialType[]>
@ -100,6 +92,25 @@ export class MixInfoForm implements OnInit {
get mixMaterialTypeId(): number {
return this.controls.materialType.value
}
get valid(): boolean {
return this.form.valid
}
}
@Component({
selector: 'cre-mix-form',
templateUrl: 'form.html'
})
export class MixForm {
@ViewChild(MixInfoForm) infoForm: MixInfoForm
@ViewChild(MixMaterialsForm) mixMaterialsForm: MixMaterialsForm
@Input() recipe: Recipe
@Input() materialTypes: Observable<MaterialType[]>
@Input() materials: Observable<Material[]>
get valid(): boolean {
return this.infoForm?.valid && this.mixMaterialsForm?.valid
}
}

View File

@ -25,10 +25,10 @@ import {CreForm, ICreForm} from '../shared/components/forms/forms';
encapsulation: ViewEncapsulation.None
})
export class RecipeForm extends SubscribingComponent {
@Input() recipe: Recipe | null
@ViewChild(CreForm) creForm: ICreForm
@Input() recipe: Recipe | null
@Output() submitForm = new EventEmitter<Recipe>();
controls: any

View File

@ -1,4 +1,4 @@
<mat-form-field>
<mat-form-field *ngIf="internalControl">
<mat-label>{{label}}</mat-label>
<input
matInput
@ -16,8 +16,8 @@
</mat-error>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let entry of getEntries()" [value]="entry.value">
{{entry.value}}
<mat-option *ngFor="let entry of filteredEntries" [value]="entry.value">
{{entry.display ? entry.display : entry.value}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

View File

@ -16,7 +16,7 @@ import {
import {AbstractControl, FormControl, ValidationErrors, ValidatorFn, Validators} from '@angular/forms'
import {COMMA, ENTER} from '@angular/cdk/keycodes'
import {isObservable, Observable, Subject} from 'rxjs'
import {map, takeUntil} from 'rxjs/operators'
import {map, startWith, takeUntil} from 'rxjs/operators'
import {MatChipInputEvent} from '@angular/material/chips'
import {MatAutocomplete, MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'
@ -55,7 +55,7 @@ export class CreInputComponent extends _CreInputBase implements AfterViewInit {
constructor(
private cdRef: ChangeDetectorRef
) {
super();
super()
}
ngAfterViewInit() {
@ -165,35 +165,37 @@ export class CreComboBoxComponent {
@ContentChild(TemplateRef) errors: TemplateRef<any>
internalControl: FormControl
filteredEntries: CreInputEntry[]
validValue = false
private _destroy$ = new Subject<boolean>();
private _destroy$ = new Subject<boolean>()
private _entries: CreInputEntry[]
private _controlsInitialized = false
@Input()
set entries(entries: Observable<CreInputEntry[]> | CreInputEntry[]) {
if (isObservable(this.entries)) {
if (isObservable(entries)) {
(entries as Observable<CreInputEntry[]>).pipe(takeUntil(this._destroy$))
.subscribe({
next: entries => {
this._entries = entries
this.initControls(entries)
}
})
} else {
this._entries = (entries as CreInputEntry[])
this.initControls((entries as CreInputEntry[]))
}
}
this.initControls()
reloadEntries() {
this.filteredEntries = this.filterEntries(this.internalControl.value)
}
getEntries(): CreInputEntry[] {
return this._entries
private initControls(entries) {
this._entries = entries
if (this._controlsInitialized) {
return
}
private initControls() {
if (this._controlsInitialized) return
if (this.control.value) {
this.internalControl.setValue(this.findEntryByKey(this.control.value)?.value)
}
@ -206,7 +208,8 @@ export class CreComboBoxComponent {
value: null,
disabled: false
}, Validators.compose([this.control.validator, this.valueValidator()]))
this.internalControl.valueChanges.pipe(takeUntil(this._destroy$))
this.internalControl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe({
next: value => {
if (this.internalControl.valid) {
@ -214,12 +217,30 @@ export class CreComboBoxComponent {
} else {
this.control.setValue(null)
}
this.filteredEntries = this.filterEntries(value)
}
})
this.reloadEntries()
this._controlsInitialized = true
}
private filterEntries(value: string): CreInputEntry[] {
if (!value) {
return this._entries
}
const valueLowerCase = value.toLowerCase()
return this._entries.filter(entry => {
if (entry.display) {
return entry.display.toLowerCase().includes(valueLowerCase)
} else {
return entry.value.toLowerCase().includes(valueLowerCase)
}
})
}
private findEntryByKey(key: any): CreInputEntry | null {
const found = this._entries.filter(e => e.key === key)
if (found.length <= 0) {