Ajout des recettes et des mélanges au frontend Angular.

This commit is contained in:
FyloZ 2021-02-05 17:30:18 -05:00
parent 8fce20f978
commit 68b6ee8855
83 changed files with 2088 additions and 153 deletions

View File

@ -6596,6 +6596,11 @@
}
}
},
"js-joda": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/js-joda/-/js-joda-1.11.0.tgz",
"integrity": "sha512-/HJpRhwP2fPyuSsCaZuoVJuaSIt8tUXykV4wOMRXrFk7RP9h9VWaFdS9YHKdMepxb/3TdXpL6IhfC9L0sqYVBw=="
},
"js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
@ -7121,6 +7126,11 @@
"object-visit": "^1.0.0"
}
},
"material-design-icons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz",
"integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",

View File

@ -24,6 +24,8 @@
"@mdi/angular-material": "^5.7.55",
"bootstrap": "^4.5.2",
"copy-webpack-plugin": "^6.2.1",
"js-joda": "^1.11.0",
"material-design-icons": "^3.0.1",
"ngx-material-file-input": "^2.1.1",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",

View File

@ -3,6 +3,7 @@ import {isPlatformBrowser} from "@angular/common";
import {AppState} from "./modules/shared/app-state";
import {Observable} from "rxjs";
import {SubscribingComponent} from "./modules/shared/components/subscribing.component";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-root',
@ -15,9 +16,11 @@ export class AppComponent extends SubscribingComponent {
constructor(
@Inject(PLATFORM_ID) private platformId: object,
private appState: AppState
private appState: AppState,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit() {

View File

@ -6,8 +6,8 @@ import {AppComponent} from './app.component';
import {MatIconRegistry} from "@angular/material/icon";
import {SharedModule} from "./modules/shared/shared.module";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import { CatalogComponent } from './pages/catalog/catalog.component';
import { CompanyModule } from './modules/company/company.module';
import {CatalogComponent} from './pages/catalog/catalog.component';
import {CompanyModule} from './modules/company/company.module';
@NgModule({
declarations: [

View File

@ -24,7 +24,7 @@ export class LoginComponent implements OnInit {
ngOnInit(): void {
if (this.accountService.isLoggedIn()) {
this.router.navigate(['/'])
this.router.navigate(['/color'])
}
this.idFormControl = this.formBuilder.control(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$'))]))

View File

@ -1,12 +1,39 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ListComponent} from "./pages/list/list.component";
import {AddComponent} from "./pages/add/add.component";
import {EditComponent} from "./pages/edit/edit.component";
import {ExploreComponent} from "./pages/explore/explore.component";
import {MixEditComponent} from "./pages/mix/mix-edit/mix-edit.component";
import {MixAddComponent} from "./pages/mix/mix-add/mix-add.component";
import { ColorsComponent } from './colors.component';
const routes: Routes = [{ path: '', component: ColorsComponent }];
const routes: Routes = [{
path: 'list',
component: ListComponent
}, {
path: 'add',
component: AddComponent
}, {
path: 'edit/:id',
component: EditComponent
}, {
path: 'add/mix/:recipeId',
component: MixAddComponent
}, {
path: 'edit/mix/:recipeId/:id',
component: MixEditComponent
}, {
path: 'explore/:id',
component: ExploreComponent
}, {
path: '',
pathMatch: 'full',
redirectTo: 'list'
}]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ColorsRoutingModule { }
export class ColorsRoutingModule {
}

View File

@ -1 +0,0 @@
<p>colors works!</p>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ColorsComponent } from './colors.component';
describe('ColorsComponent', () => {
let component: ColorsComponent;
let fixture: ComponentFixture<ColorsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ColorsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ColorsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,15 +0,0 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'cre-colors',
templateUrl: './colors.component.html',
styleUrls: ['./colors.component.sass']
})
export class ColorsComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -1,15 +1,31 @@
import {NgModule} from '@angular/core';
import {ColorsRoutingModule} from './colors-routing.module';
import {ColorsComponent} from './colors.component';
import {SharedModule} from "../shared/shared.module";
import {ListComponent} from './pages/list/list.component';
import {AddComponent} from './pages/add/add.component';
import {EditComponent} from './pages/edit/edit.component';
import {MatExpansionModule} from "@angular/material/expansion";
import {FormsModule} from "@angular/forms";
import {ExploreComponent} from './pages/explore/explore.component';
import {RecipeInfoComponent} from './components/recipe-info/recipe-info.component';
import {MixTableComponent} from './components/mix-table/mix-table.component';
import {StepListComponent} from './components/step-list/step-list.component';
import {StepTableComponent} from './components/step-table/step-table.component';
import {MixEditorComponent} from './components/mix-editor/mix-editor.component';
import {UnitSelectorComponent} from './components/unit-selector/unit-selector.component';
import {MixAddComponent} from './pages/mix/mix-add/mix-add.component';
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component';
@NgModule({
declarations: [ColorsComponent],
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent],
imports: [
ColorsRoutingModule,
SharedModule
SharedModule,
MatExpansionModule,
FormsModule
]
})
export class ColorsModule { }
export class ColorsModule {
}

View File

@ -0,0 +1,96 @@
<mat-card *ngIf="recipe" class="x-centered y-centered">
<mat-card-header>
<mat-card-title *ngIf="!editionMode">Création d'un mélange pour la recette {{recipe.company.name}}
- {{recipe.name}}</mat-card-title>
<mat-card-title *ngIf="editionMode">Modification du mélange {{mix.mixType.name}} de la
recette {{recipe.company.name}} - {{recipe.name}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-form-field>
<mat-label>Nom</mat-label>
<input matInput type="text" [formControl]="nameControl"/>
<mat-icon svgIcon="form-textbox" matSuffix></mat-icon>
</mat-form-field>
<mat-form-field>
<mat-label>Type de produit</mat-label>
<mat-select [formControl]="materialTypeControl">
<mat-option
*ngFor="let materialType of (materialTypes$ | async)"
[value]="materialType.id">
{{materialType.name}}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngTemplateOutlet="mixEditor"></ng-container>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" routerLink="/color/edit/{{recipeId}}">Retour</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete()">Supprimer</button>
<button mat-raised-button color="accent" [disabled]="!form.valid" (click)="submit()">Enregistrer</button>
</mat-card-actions>
</mat-card>
<ng-template #mixEditor>
<table #mixTable mat-table [dataSource]="mixMaterials">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let mixMaterial; let i = index">
{{i + 1}}
</td>
</ng-container>
<ng-container matColumnDef="material">
<th mat-header-cell *matHeaderCellDef>Produit</th>
<td mat-cell *matCellDef="let mixMaterial">
<mat-form-field *ngIf="materials">
<mat-select [(ngModel)]="mixMaterial.materialId">
<mat-option *ngFor="let material of materials"
[value]="material.id">{{material.name}}</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let mixMaterial">
<mat-form-field>
<input matInput type="number" step="0.001" [(ngModel)]="mixMaterial.quantity"/>
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="units">
<th mat-header-cell *matHeaderCellDef>Unités</th>
<td mat-cell *matCellDef="let mixMaterial" class="units-wrapper">
<ng-container *ngIf="materials">
<ng-container *ngIf="materialUsePercentages(mixMaterial)">%</ng-container>
<ng-container *ngIf="!materialUsePercentages(mixMaterial)">
<ng-container *ngIf="!hoveredMixMaterial || hoveredMixMaterial != mixMaterial">
<span>{{units}}</span>
</ng-container>
<ng-container *ngIf="hoveredMixMaterial && hoveredMixMaterial == mixMaterial">
<cre-unit-selector [(unit)]="units" [showLabel]="false" [short]="true"></cre-unit-selector>
</ng-container>
</ng-container>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="buttonRemove">
<th mat-header-cell *matHeaderCellDef>
<button mat-raised-button color="accent" (click)="addRow()">Ajouter</button>
</th>
<td mat-cell *matCellDef="let mixMaterial; let i = index">
<ng-container *ngIf="hoveredMixMaterial && hoveredMixMaterial == mixMaterial">
<button mat-raised-button color="warn" (click)="removeRow(i)">Retirer</button>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let mixMaterial; columns: columns" (mouseover)="hoveredMixMaterial = mixMaterial"></tr>
</table>
</ng-template>

View File

@ -0,0 +1,2 @@
td.units-wrapper
width: 3rem

View File

@ -0,0 +1,132 @@
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 {MixService} from "../../services/mix.service";
import {Observable} from "rxjs";
import {RecipeService} from "../../services/recipe.service";
import {Material} from "../../../shared/model/material.model";
import {MaterialService} from "../../../material/service/material.service";
import {MaterialType} from "../../../shared/model/materialtype.model";
import {MaterialTypeService} from "../../../material-type/service/material-type.service";
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {UNIT_MILLILITER} from "../../../shared/units";
import {MatTable} from "@angular/material/table";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-mix-editor',
templateUrl: './mix-editor.component.html',
styleUrls: ['./mix-editor.component.sass']
})
export class MixEditorComponent extends SubscribingComponent {
@ViewChild('mixTable') mixTable: MatTable<MixMaterial>
@Input() mixId: number | null
@Input() recipeId: number | null
@Input() materials: Material[]
@Output() save = new EventEmitter<any>();
mix: Mix | null
recipe: Recipe | null
materialTypes$: Observable<MaterialType[]>
form: FormGroup
nameControl: FormControl
materialTypeControl: FormControl
mixMaterials = []
editionMode = false
units = UNIT_MILLILITER
hoveredMixMaterial: MixMaterial | null
columns = ['position', 'material', 'quantity', 'units', 'buttonRemove']
constructor(
private mixService: MixService,
private recipeService: RecipeService,
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
private formBuilder: FormBuilder,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit() {
super.ngOnInit();
this.mixId = this.urlUtils.parseIntUrlParam('id')
this.subscribe(
this.recipeService.getById(this.recipeId),
{
next: r => {
this.recipe = r
if (this.mixId) {
this.editionMode = true
this.subscribe(
this.mixService.getById(this.mixId),
{
next: m => {
this.mix = m
this.mixMaterials = this.mixService.extractMixMaterials(this.mix)
this.generateForm()
}, error: err => this.handleNotFoundError(err, '/color/list')
}
)
} else {
this.mixMaterials.push({})
this.generateForm()
}
},
error: err => this.handleNotFoundError(err, '/color/list')
}
)
this.materialTypes$ = this.materialTypeService.all
}
addRow() {
this.mixMaterials.push({materialId: null, quantity: null, percents: false})
this.mixTable.renderRows()
}
removeRow(position: number) {
this.mixMaterials.splice(position, 1)
this.mixTable.renderRows()
}
submit() {
this.save.emit({
name: this.nameControl.value,
recipeId: this.recipeId,
materialTypeId: this.materialTypeControl.value,
mixMaterials: this.mixMaterials,
units: this.units
})
}
delete() {
}
materialUsePercentages(mixMaterial: any) {
if (!mixMaterial.materialId) return null
const material = this.getMaterialFromId(mixMaterial.materialId);
mixMaterial.percents = material && material.materialType.usePercentages
return mixMaterial.percents
}
getMaterialFromId(id: number): Material {
return id ? this.materials.filter(m => m.id === id)[0] : null
}
private generateForm() {
this.nameControl = new FormControl(this.mix ? this.mix.mixType.name : null, Validators.required)
this.materialTypeControl = new FormControl(this.mix ? this.mix.mixType.material.materialType.id : null, Validators.required)
this.form = this.formBuilder.group({
name: this.nameControl,
materialType: this.materialTypeControl
})
}
}

View File

@ -0,0 +1,100 @@
<mat-expansion-panel class="table-title mt-5">
<mat-expansion-panel-header>
<mat-panel-title>{{mix.mixType.name}}</mat-panel-title>
</mat-expansion-panel-header>
<div class="mix-actions d-flex flex-row justify-content-between">
<ng-container *ngIf="!editing">
<div class="flex-grow-1">
<mat-form-field class="dark">
<mat-label>Casier</mat-label>
<input matInput type="text" [(ngModel)]="mix.location" (change)="changeLocation($event)"/>
</mat-form-field>
</div>
<div>
<button mat-raised-button color="accent">Imprimer</button>
</div>
<div>
<button mat-raised-button color="accent" (click)="deduct.emit()">Déduire</button>
</div>
</ng-container>
<ng-container *ngIf="editing">
<div class="flex-grow-1"></div>
<button mat-raised-button color="accent" routerLink="/color/edit/mix/{{recipe.id}}/{{mix.id}}">Modifier</button>
</ng-container>
</div>
<ng-container [ngTemplateOutlet]="mixTable"></ng-container>
</mat-expansion-panel>
<ng-template #mixTable>
<table mat-table [dataSource]="mix.mixMaterials">
<ng-container matColumnDef="material">
<th mat-header-cell *matHeaderCellDef>Produit</th>
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.material.name}}</td>
<td mat-footer-cell *matFooterCellDef></td>
</ng-container>
<ng-container matColumnDef="materialType">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.material.materialType.name}}</td>
<td mat-footer-cell *matFooterCellDef></td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let mixMaterial">
<mat-form-field>
<input
matInput
type="number"
min="0.001"
step="0.001"
[value]="getComputedQuantityRounded(mixMaterial)"
[disabled]="mixMaterial.material.materialType.usePercentages"
(change)="changeQuantity($event, mixMaterial)"/>
</mat-form-field>
</td>
<td mat-footer-cell *matFooterCellDef>
<mat-form-field>
<mat-label>Total</mat-label>
<input
matInput
type="number"
min="0.001"
step="0.001"
[value]="round(getTotalQuantity())"
(change)="changeQuantity($event, null, true)"/>
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="quantityStatic">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let mixMaterial">{{getComputedQuantityRounded(mixMaterial)}}</td>
</ng-container>
<ng-container matColumnDef="quantityCalculated">
<th mat-header-cell *matHeaderCellDef>Calcul</th>
<td mat-cell *matCellDef="let mixMaterial; let i = index" [innerHTML]="getCalculatedQuantity(mixMaterial, i)"
class="mix-calculation"></td>
<td mat-footer-cell *matFooterCellDef></td>
</ng-container>
<ng-container matColumnDef="quantityUnits">
<th mat-header-cell *matHeaderCellDef>Unités</th>
<td mat-cell *matCellDef="let mixMaterial">
<ng-container *ngIf="mixMaterial.material.materialType.usePercentages">%</ng-container>
<ng-container *ngIf="!mixMaterial.material.materialType.usePercentages">{{units}}</ng-container>
</td>
<td mat-footer-cell *matFooterCellDef>{{units}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="mixColumns"></tr>
<tr mat-row *matRowDef="let mixMaterial; columns: mixColumns"
[class.low-quantity]="!editing && isInLowQuantity(mixMaterial.id)"></tr>
<ng-container *ngIf="!editing">
<tr mat-footer-row *matFooterRowDef="mixColumns"></tr>
</ng-container>
</table>
</ng-template>

View File

@ -0,0 +1,22 @@
@import '../../../../../custom-theme'
mat-expansion-panel
width: 40rem
margin: 2rem 0
.mix-actions
background-color: $color-primary
padding: 0 1rem
div:last-child
margin-left: 1rem
.low-quantity
background-color: #ffb3b3
::ng-deep span.mix-calculated-quantity
&:first-child
color: green
&:last-child
color: dimgrey

View File

@ -0,0 +1,131 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Mix, MixMaterial, Recipe} from "../../../shared/model/recipe.model";
import {Subject} from "rxjs";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {convertMixMaterialQuantity, UNIT_MILLILITER, UNIT_RATIOS} from "../../../shared/units";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-mix-table',
templateUrl: './mix-table.component.html',
styleUrls: ['./mix-table.component.sass']
})
export class MixTableComponent extends SubscribingComponent {
private readonly COLUMNS = ['material', 'materialType', 'quantity', 'quantityCalculated', 'quantityUnits']
private readonly COLUMNS_STATIC = ['material', 'materialType', 'quantityStatic', 'quantityUnits']
@Input() mix: Mix
@Input() recipe: Recipe
@Input() units$: Subject<string>
@Input() deductErrorBody
@Input() editing: boolean
@Output() locationChange = new EventEmitter<{ id: number, location: string }>()
@Output() quantityChange = new EventEmitter<{ id: number, materialId: number, quantity: number }>()
@Output() deduct = new EventEmitter<void>()
mixColumns = this.COLUMNS
units = UNIT_MILLILITER
computedQuantities: { id: number, percents: boolean, quantity: number }[] = []
constructor(
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit() {
super.ngOnInit();
if (this.editing) {
this.mixColumns = this.COLUMNS_STATIC
}
this.mix.mixMaterials.forEach(m => this.computedQuantities.push({
id: m.id,
percents: m.material.materialType.usePercentages,
quantity: m.quantity
}))
this.subscribe(
this.units$,
{
next: u => this.convertQuantities(u)
}
)
}
changeLocation(event: any) {
this.locationChange.emit({id: this.mix.id, location: event.target.value})
}
changeQuantity(event: any, mixMaterial: MixMaterial, isTotal = false) {
const newQuantity = parseInt(event.target.value)
let ratio = 1
if (!isTotal) {
const originalQuantity = this.getComputedQuantity(mixMaterial.id)
ratio = newQuantity / originalQuantity.quantity
} else {
ratio = newQuantity / this.getTotalQuantity()
}
this.computedQuantities.forEach((q, i) => {
if (!q.percents) {
q.quantity *= ratio
}
this.emitQuantityChangeEvent(i)
})
}
getComputedQuantityRounded(mixMaterial: MixMaterial): number {
return this.round(this.getComputedQuantity(mixMaterial.id).quantity)
}
getTotalQuantity(index: number = -1): number {
if (index === -1) index = this.computedQuantities.length - 1
let totalQuantity = 0
for (let i = 0; i <= index; i++) {
totalQuantity += this.calculateQuantity(i)
}
return totalQuantity
}
getCalculatedQuantity(mixMaterial: MixMaterial, index: number): string {
const totalQuantity = this.round(this.getTotalQuantity(index))
const addedQuantity = this.round(this.calculateQuantity(index))
return `<span class="mix-calculated-quantity">+${addedQuantity}</span> <span class="mix-calculated-quantity">(${totalQuantity})</span>`
}
isInLowQuantity(materialId: number): boolean {
return this.deductErrorBody[this.mix.id] && this.deductErrorBody[this.mix.id].indexOf(materialId) >= 0
}
round(quantity: number): number {
return Math.round(quantity * 1000) / 1000
}
private emitQuantityChangeEvent(index: number) {
this.quantityChange.emit({
id: this.mix.id,
materialId: this.computedQuantities[index].id,
quantity: this.calculateQuantity(index)
})
}
private convertQuantities(newUnit: string) {
this.computedQuantities.forEach(q => convertMixMaterialQuantity(q, this.units, newUnit))
this.units = newUnit
}
private getComputedQuantity(id: number): any {
return this.computedQuantities.filter(q => q.id == id)[0]
}
private calculateQuantity(index: number): number {
const computedQuantity = this.computedQuantities[index]
if (!computedQuantity.percents) {
return computedQuantity.quantity
}
return this.computedQuantities[0].quantity * (computedQuantity.quantity / 100)
}
}

View File

@ -0,0 +1,29 @@
<div class="recipe-info-wrapper d-flex flex-column">
<div class="d-flex flex-column">
<h3>{{recipe.company.name}} - {{recipe.name}}</h3>
</div>
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<p>Échantillon #{{recipe.sample}}</p>
<p *ngIf="recipe.approbationDate">Approuvée le {{recipe.approbationDate}}</p>
<div *ngIf="!recipe.approbationDate" class="recipe-not-approved-wrapper d-flex flex-row">
<p>Non approuvée</p>
<mat-icon svgIcon="alert" class="color-warning"></mat-icon>
</div>
<p>{{recipe.remark}}</p>
</div>
<div class="recipe-description">
<p>{{recipe.description}}</p>
</div>
<div class="flex-grow-1"></div>
<mat-icon
*ngIf="hasModifications"
class="color-warning has-modification-icon"
svgIcon="pencil"
title="Les modifications apportées n'ont pas été enregistrées"
[inline]="true">
</mat-icon>
</div>
</div>

View File

@ -0,0 +1,46 @@
.recipe-info-wrapper
background-color: black
color: white
padding: 1rem
div
margin-right: 3rem
p
margin-bottom: 0
h3
font-weight: bold
text-decoration: underline
text-transform: uppercase
&.recipe-not-approved-wrapper
p
margin-top: 1px
margin-right: .4em
&.recipe-description
max-width: 30rem
&.recipe-note
margin-right: 0
mat-form-field
width: 100%
::ng-deep .mat-form-field-wrapper
padding-bottom: 0
::ng-deep .mat-form-field-underline, ::ng-deep .mat-form-field-ripple
opacity: 0
mat-label
color: white
textarea:focus
background-color: white
color: black
.has-modification-icon
width: 2em !important

View File

@ -0,0 +1,14 @@
import {Component, Input} from '@angular/core';
import {Recipe} from "../../../shared/model/recipe.model";
@Component({
selector: 'cre-recipe-info',
templateUrl: './recipe-info.component.html',
styleUrls: ['./recipe-info.component.sass']
})
export class RecipeInfoComponent {
@Input() recipe: Recipe
@Input() hasModifications: boolean
constructor() { }
}

View File

@ -0,0 +1,12 @@
<mat-card class="mt-5">
<mat-card-header>
<mat-card-title>Étapes</mat-card-title>
</mat-card-header>
<mat-card-content class="no-action">
<mat-list>
<mat-list-item *ngFor="let step of steps;let i = index">
{{i + 1}}.<span class="space"></span>{{step.message}}
</mat-list-item>
</mat-list>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,2 @@
.space
width: 1em

View File

@ -0,0 +1,11 @@
import {Component, Input} from '@angular/core';
import {RecipeStep} from "../../../shared/model/recipe.model";
@Component({
selector: 'cre-step-list',
templateUrl: './step-list.component.html',
styleUrls: ['./step-list.component.sass']
})
export class StepListComponent {
@Input() steps: RecipeStep[]
}

View File

@ -0,0 +1,33 @@
<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>
<table #stepTable mat-table [dataSource]="steps">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let step; let i = index">{{i + 1}}</td>
</ng-container>
<ng-container matColumnDef="message">
<th mat-header-cell *matHeaderCellDef>Message</th>
<td mat-cell *matCellDef="let step">
<mat-form-field>
<input matInput type="text" [(ngModel)]="step.message"/>
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="buttonRemove">
<th mat-header-cell *matHeaderCellDef>
<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>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let step; columns: columns"></tr>
</table>
</mat-expansion-panel>

View File

@ -0,0 +1,5 @@
mat-expansion-panel
min-width: 560px
mat-form-field
width: 20rem

View File

@ -0,0 +1,25 @@
import {Component, Input, ViewChild} from '@angular/core';
import {RecipeStep} from "../../../shared/model/recipe.model";
import {MatTable} from "@angular/material/table";
@Component({
selector: 'cre-step-table',
templateUrl: './step-table.component.html',
styleUrls: ['./step-table.component.sass']
})
export class StepTableComponent {
@ViewChild('stepTable', {static: true}) stepTable: MatTable<RecipeStep>
readonly columns = ['position', 'message', 'buttonRemove']
@Input() steps: RecipeStep[]
addStep() {
this.steps.push({id: null, message: ""})
this.stepTable.renderRows()
}
removeStep(position: number) {
this.steps.splice(position, 1)
this.stepTable.renderRows()
}
}

View File

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

View File

@ -0,0 +1,2 @@
mat-form-field.short
width: 3rem

View File

@ -0,0 +1,17 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
@Component({
selector: 'cre-unit-selector',
templateUrl: './unit-selector.component.html',
styleUrls: ['./unit-selector.component.sass']
})
export class UnitSelectorComponent {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
@Input() unit = UNIT_MILLILITER
@Input() showLabel = true
@Input() short = false
@Output() unitChange = new EventEmitter<string>()
}

View File

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

View File

@ -0,0 +1,98 @@
import {Component} from '@angular/core';
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {RecipeService} from "../../services/recipe.service";
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {FormBuilder, Validators} from "@angular/forms";
import {CompanyService} from "../../../company/service/company.service";
import {map} from "rxjs/operators";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-add',
templateUrl: './add.component.html',
styleUrls: ['./add.component.sass']
})
export class AddComponent extends SubscribingComponent {
formFields: FormField[] = [
{
name: 'name',
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
},
{
name: 'description',
label: 'Description',
icon: 'text',
type: 'text',
validator: Validators.required,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
},
{
name: 'sample',
label: 'Échantillon',
icon: 'pound',
type: 'number',
validator: Validators.compose([Validators.required, Validators.min(0)]),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un numéro d\'échantillon est requis'},
{conditionFn: errors => errors.min, message: 'Le numéro d\'échantillon doit être supérieur ou égal à 0'}
]
},
{
name: 'approbationDate',
label: 'Date d\'approbation',
icon: 'calendar',
type: 'date'
},
{
name: 'remark',
label: 'Remarque',
icon: 'text',
type: 'text'
},
{
name: 'company',
label: 'Bannière',
icon: 'domain',
type: 'select',
validator: Validators.required,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une bannière est requise'}
],
options$: this.companyService.all.pipe(map(companies => companies.map(c => {
return {value: c.id, label: c.name}
})))
}
]
unknownError = false
errorMessage: string | null
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
submit(values) {
this.subscribe(
this.recipeService.save(values.name, values.description, values.sample, values.approbationDate, values.remark, values.company),
{
next: recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`),
error: err => {
this.unknownError = true
console.error(err)
}
}
)
}
}

View File

@ -0,0 +1,68 @@
<div *ngIf="recipe">
<div *ngIf="!recipe.mixes" class="alert alert-warning m-3">
<p>Il n'y a aucun mélange dans cette recette</p>
</div>
<div *ngIf="recipe.steps.length <= 0" class="alert alert-warning m-3">
<p>Il n'y a aucune étape dans cette recette</p>
</div>
<div class="action-bar backward">
<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)">Enregistrer</button>
<button mat-raised-button color="warn" *ngIf="hasDeletePermission" (click)="delete()">Supprimer</button>
</div>
<mat-form-field>
<mat-label>Unités</mat-label>
<mat-select [value]="unitConstants.UNIT_MILLILITER" (selectionChange)="changeUnits($event.value)">
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
<mat-option [value]="unitConstants.UNIT_GALLON">Gallons</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex-grow-1"></div>
</div>
<div class="recipe-wrapper d-flex flex-row justify-content-around align-items-start flex-wrap">
<div>
<cre-entity-edit
#editComponent
title="Modifier la couleur {{recipe.name}}"
deleteConfirmMessage="Voulez-vous vraiment supprimer la couleur {{recipe.name}}?"
[entity]="recipe"
[formFields]="formFields"
[unknownError]="unknownError"
[customError]="errorMessage"
[disableButtons]="true">
</cre-entity-edit>
</div>
<div class="recipe-mixes-wrapper">
<mat-card>
<mat-card-header>
<mat-card-title>Mélanges</mat-card-title>
</mat-card-header>
<mat-card-content class="no-action pl-5">
<ng-container *ngFor="let mix of recipe.mixes; let i = index">
<cre-mix-table
[class.no-top-margin]="i == 0"
[recipe]="recipe"
[mix]="mix"
[units$]="units$"
[editing]="true">
</cre-mix-table>
</ng-container>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="accent" routerLink="/color/add/mix/{{recipe.id}}">Ajouter</button>
</mat-card-actions>
</mat-card>
</div>
<div class="mt-5">
<cre-step-table [steps]="recipe.steps"></cre-step-table>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
.recipe-wrapper > div
margin: 0 3rem 3rem
mat-card
margin-top: 3rem
.recipe-mixes-wrapper mat-card
min-width: 20rem
mat-card
mat-card-content
margin-bottom: 0
padding-top: 16px !important
padding-bottom: 0 !important
mat-card-actions
margin-top: 0

View File

@ -0,0 +1,142 @@
import {Component} from '@angular/core';
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {Recipe} from "../../../shared/model/recipe.model";
import {RecipeService} from "../../services/recipe.service";
import {ActivatedRoute, Router} from "@angular/router";
import {FormBuilder, Validators} from "@angular/forms";
import {Subject} from "rxjs";
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
import {AccountService} from "../../../accounts/services/account.service";
import {EmployeePermission} from "../../../shared/model/employee";
import {EntityEditComponent} from "../../../shared/components/entity-edit/entity-edit.component";
@Component({
selector: 'cre-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.sass']
})
export class EditComponent extends SubscribingComponent {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
recipe: Recipe | null
formFields = [
{
name: 'name',
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
},
{
name: 'description',
label: 'Description',
icon: 'text',
type: 'text',
validator: Validators.required,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
},
{
name: 'sample',
label: 'Échantillon',
icon: 'pound',
type: 'number',
validator: Validators.compose([Validators.required, Validators.min(0)]),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un numéro d\'échantillon est requis'},
{conditionFn: errors => errors.min, message: 'Le numéro d\'échantillon doit être supérieur ou égal à 0'}
]
},
{
name: 'approbationDate',
label: 'Date d\'approbation',
icon: 'calendar',
type: 'date'
},
{
name: 'remark',
label: 'Remarque',
icon: 'text',
type: 'text'
},
{
name: 'company',
label: 'Bannière',
icon: 'domain',
type: 'text',
readonly: true,
valueFn: recipe => recipe.company.name,
}
]
unknownError = false
errorMessage: string | null
units$ = new Subject<string>()
constructor(
private recipeService: RecipeService,
private accountService: AccountService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit() {
super.ngOnInit();
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
this.subscribe(
this.recipeService.getById(id),
{
next: recipe => this.recipe = recipe,
error: err => {
if (err.status === 404) {
this.router.navigate(['/color/list'])
} else {
this.unknownError = true
}
}
},
1
)
}
changeUnits(unit: string) {
this.units$.next(unit)
}
submit(editComponent: EntityEditComponent) {
const values = editComponent.values
this.subscribe(
this.recipeService.update(this.recipe.id, values.name, values.description, values.sample, values.approbationDate, values.remark, this.recipe.steps),
{
next: () => this.router.navigate(['/color/list']),
error: err => {
if (err.status === 409) {
this.errorMessage = `Une couleur avec le nom '${values.name}' et la bannière '${this.recipe.company.name}' existe déjà`
} else {
this.unknownError = true
console.error(err)
}
}
}
)
}
delete() {
this.subscribe(
this.recipeService.delete(this.recipe.id),
{
next: () => this.router.navigate(['/color/list'])
}
)
}
get hasDeletePermission(): boolean {
return this.accountService.hasPermission(EmployeePermission.REMOVE_RECIPE)
}
}

View File

@ -0,0 +1,59 @@
<div *ngIf="recipe">
<cre-recipe-info [recipe]="recipe" [hasModifications]="hasModifications"></cre-recipe-info>
<div *ngIf="error" class="alert alert-danger m-3">
<p *ngIf="error === ERROR_UNKNOWN">Une erreur est survenue</p>
<p *ngIf="error === ERROR_DEDUCT">Certains produit ne sont pas en quantité suffisante dans l'inventaire</p>
</div>
<div *ngIf="success" class="alert alert-success m-3">
<p *ngIf="success === SUCCESS_SAVE">Les modifications ont été enregistrées</p>
<p *ngIf="success === SUCCESS_DEDUCT">Les quantités des produits utilisés ont été déduites de l'inventaire</p>
</div>
<div class="action-bar backward d-flex flex-row">
<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="accent" (click)="saveModifications()" [disabled]="!hasModifications">
Enregistrer
</button>
<button mat-raised-button color="accent" (click)="deductQuantities()">Déduire</button>
</div>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
</div>
<div class="flex-grow-1"></div>
<mat-form-field class="w-auto">
<mat-label>Note</mat-label>
<textarea matInput cols="40" rows="3" (change)="changeNote($event)">{{note}}</textarea>
</mat-form-field>
</div>
<div class="recipe-content">
<!-- Mixes -->
<div>
<ng-container *ngFor="let mix of recipe.mixes">
<cre-mix-table
[mix]="mix"
[deductErrorBody]="deductErrorBody"
[units$]="units$"
(quantityChange)="changeQuantity($event)"
(locationChange)="changeMixLocation($event)"
(deduct)="deductMixQuantities(mix.id)">
</cre-mix-table>
</ng-container>
</div>
<!-- Steps -->
<div>
<cre-step-list [steps]="recipe.steps"></cre-step-list>
</div>
</div>
<!-- Images -->
<div class="recipe-images">
Images
</div>
</div>

View File

@ -0,0 +1,21 @@
.recipe-content
display: flex
flex-direction: column
align-items: center
div
margin-bottom: 1rem
.recipe-images
margin-top: 5rem
padding: 2rem
background-color: grey
@media screen and (min-width: 1920px)
.action-bar
padding-bottom: 5rem
.recipe-content
flex-direction: row
justify-content: space-around
align-items: start

View File

@ -0,0 +1,135 @@
import {Component} from '@angular/core';
import {RecipeService} from "../../services/recipe.service";
import {ActivatedRoute, Router} from "@angular/router";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {Recipe} from "../../../shared/model/recipe.model";
import {Observable, Subject} from "rxjs";
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
import {FormBuilder} from "@angular/forms";
@Component({
selector: 'cre-explore',
templateUrl: './explore.component.html',
styleUrls: ['./explore.component.sass']
})
export class ExploreComponent extends SubscribingComponent {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
recipe: Recipe | null
error: string
deductErrorBody = {}
success: string
units$ = new Subject<string>()
hasModifications = false
note: string | null
quantitiesChanges = new Map<number, Map<number, number>>()
mixesLocationChanges = new Map<number, string>()
// Errors
readonly ERROR_UNKNOWN = 'unknown'
readonly ERROR_DEDUCT = 'deduct'
// Success
readonly SUCCESS_SAVE = 'save'
readonly SUCCESS_DEDUCT = 'deduct'
constructor(
private recipeService: RecipeService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit(): void {
super.ngOnInit()
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
this.subscribe(
this.recipeService.getById(id),
{
next: r => {
this.recipe = r
this.note = r.note
},
error: err => {
if (err.status == 404) {
this.router.navigate(['/colors/list'])
} else {
this.error = this.ERROR_UNKNOWN
}
}
},
1
)
}
changeUnits(unit: string) {
this.units$.next(unit)
}
changeNote(event: any) {
this.hasModifications = true
this.note = event.target.value
}
changeQuantity(event: { id: number, materialId: number, quantity: number }) {
if (!this.quantitiesChanges.has(event.id)) this.quantitiesChanges.set(event.id, new Map<number, number>())
this.quantitiesChanges.get(event.id).set(event.materialId, event.quantity)
}
changeMixLocation(event: { id: number, location: string }) {
this.hasModifications = true
this.mixesLocationChanges.set(event.id, event.location)
}
saveModifications() {
this.subscribe(
this.recipeService.saveExplorerModifications(this.recipe.id, this.note, this.mixesLocationChanges),
{
next: () => {
this.hasModifications = false
this.error = null
this.success = this.SUCCESS_SAVE
},
error: err => {
this.success = null
this.error = this.ERROR_UNKNOWN
console.error(err)
}
},
1
)
}
deductQuantities() {
this.performDeductQuantities(this.recipeService.deductQuantities(this.recipe, this.quantitiesChanges))
}
deductMixQuantities(mixId: number) {
this.performDeductQuantities(this.recipeService.deductMixQuantities(this.recipe, mixId, this.quantitiesChanges.get(mixId)))
}
performDeductQuantities(observable: Observable<void>) {
this.subscribe(
observable,
{
next: () => {
this.error = null
this.success = this.SUCCESS_DEDUCT
},
error: err => {
this.success = null
if (err.status === 409) { // There is not enough of one or more materials in the inventory
this.error = this.ERROR_DEDUCT
this.deductErrorBody = err.error
} else {
this.error = this.ERROR_UNKNOWN
console.error(err)
}
}
},
1
)
}
}

View File

@ -0,0 +1,71 @@
<div class="action-bar">
<mat-form-field>
<mat-label>Recherche</mat-label>
<input matInput type="text" [(ngModel)]="searchQuery"/>
<button mat-button *ngIf="searchQuery" matSuffix mat-icon-button (click)="searchQuery=''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<div class="button-add">
<button *ngIf="hasEditPermission" mat-raised-button color="accent" routerLink="/color/add">Ajouter</button>
</div>
</div>
<mat-expansion-panel class="table-title" *ngFor="let companyRecipes of (recipes$ | async)"
[hidden]="isCompanyHidden(companyRecipes.recipes)" [expanded]="panelForcedExpanded">
<mat-expansion-panel-header>
<mat-panel-title>
{{companyRecipes.company}}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: companyRecipes.recipes}"></ng-container>
</mat-expansion-panel>
<ng-template
#recipeTableTemplate
let-recipes="recipes">
<table class="mx-auto" mat-table [dataSource]="recipes">
<!-- Recipe's info -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let recipe">{{recipe.name}}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Description</th>
<td mat-cell *matCellDef="let recipe">{{recipe.description}}</td>
</ng-container>
<ng-container matColumnDef="sample">
<th mat-header-cell *matHeaderCellDef>Échantillon</th>
<td mat-cell *matCellDef="let recipe">#{{recipe.sample}}</td>
</ng-container>
<!-- Icons -->
<ng-container matColumnDef="iconNotApproved">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe" [class.disabled]="recipe.approbationDate">
<mat-icon svgIcon="alert" class="color-warning" title="Cette recette n'est pas approuvée"></mat-icon>
</td>
</ng-container>
<!-- Buttons -->
<ng-container matColumnDef="buttonView">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe">
<button mat-flat-button color="accent" routerLink="/color/explore/{{recipe.id}}">Voir</button>
</td>
</ng-container>
<ng-container matColumnDef="buttonEdit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe" [class.disabled]="!hasEditPermission">
<button mat-flat-button color="accent" routerLink="/color/edit/{{recipe.id}}">Modifier</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let row; columns: tableCols" [hidden]="!searchRecipe(row)"></tr>
</table>
</ng-template>

View File

@ -0,0 +1,6 @@
mat-expansion-panel
width: 60rem
margin: 20px auto
.button-add
margin-top: .8rem

View File

@ -0,0 +1,56 @@
import {Component} from '@angular/core';
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {RecipeService} from "../../services/recipe.service";
import {EmployeePermission} from "../../../shared/model/employee";
import {AccountService} from "../../../accounts/services/account.service";
import {Recipe} from "../../../shared/model/recipe.model";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.sass']
})
export class ListComponent extends SubscribingComponent {
recipes$ = this.recipeService.allSortedByCompany
tableCols = ['name', 'description', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
searchQuery = ""
panelForcedExpanded = false
recipesHidden = []
constructor(
private recipeService: RecipeService,
private accountService: AccountService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit() {
super.ngOnInit();
}
searchRecipe(recipe: Recipe) {
if (this.searchQuery.length > 0) {
this.panelForcedExpanded = true
}
const positive = this.searchString(recipe.name) ||
this.searchString(recipe.description) ||
this.searchString(recipe.sample.toString())
this.recipesHidden[recipe.id] = !positive
return positive
}
isCompanyHidden(companyRecipes: Recipe[]): boolean {
return (this.searchQuery && this.searchQuery.length > 0) && companyRecipes.map(r => this.recipesHidden[r.id]).filter(r => !r).length <= 0
}
get hasEditPermission(): boolean {
return this.accountService.hasPermission(EmployeePermission.EDIT_RECIPE)
}
private searchString(value: string): boolean {
return value.toLowerCase().indexOf(this.searchQuery.toLowerCase()) >= 0
}
}

View File

@ -0,0 +1,5 @@
<cre-mix-editor
[recipeId]="recipeId"
[materials]="materials"
(save)="submit($event)">
</cre-mix-editor>

View File

@ -0,0 +1,43 @@
import {Component} from '@angular/core';
import {Material} from "../../../../shared/model/material.model";
import {MaterialService} from "../../../../material/service/material.service";
import {ActivatedRoute, Router} from "@angular/router";
import {SubscribingComponent} from "../../../../shared/components/subscribing.component";
import {MixService} from "../../../services/mix.service";
@Component({
selector: 'cre-mix-add',
templateUrl: './mix-add.component.html',
styleUrls: ['./mix-add.component.sass']
})
export class MixAddComponent extends SubscribingComponent {
recipeId: number | null
materials: Material[] | null
constructor(
private materialService: MaterialService,
private mixService: MixService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit(): void {
super.ngOnInit()
this.recipeId = this.urlUtils.parseIntUrlParam('recipeId')
this.subscribe(
this.materialService.getAllForMixCreation(this.recipeId),
{next: m => this.materials = m}
)
}
submit(values) {
this.subscribe(
this.mixService.saveWithUnits(values.name, values.recipeId, values.materialTypeId, values.mixMaterials, values.units),
{next: () => this.urlUtils.navigateTo(`/color/edit/${this.recipeId}`)}
)
}
}

View File

@ -0,0 +1,6 @@
<cre-mix-editor
[mixId]="mixId"
[recipeId]="recipeId"
[materials]="materials"
(save)="submit($event)">
</cre-mix-editor>

View File

@ -0,0 +1,45 @@
import {Component} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {SubscribingComponent} from "../../../../shared/components/subscribing.component";
import {Material} from "../../../../shared/model/material.model";
import {MaterialService} from "../../../../material/service/material.service";
import {MixService} from "../../../services/mix.service";
@Component({
selector: 'cre-mix-edit',
templateUrl: './mix-edit.component.html',
styleUrls: ['./mix-edit.component.sass']
})
export class MixEditComponent extends SubscribingComponent {
mixId: number | null
recipeId: number | null
materials: Material[] | null
constructor(
private materialService: MaterialService,
private mixService: MixService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit(): void {
super.ngOnInit()
this.mixId = this.urlUtils.parseIntUrlParam('id')
this.recipeId = this.urlUtils.parseIntUrlParam('recipeId')
this.subscribe(
this.materialService.getAllForMixUpdate(this.mixId),
{next: m => this.materials = m}
)
}
submit(values) {
this.subscribe(
this.mixService.updateWithUnits(this.mixId, values.name, values.materialTypeId, values.mixMaterials, values.units),
{next: () => this.urlUtils.navigateTo(`/color/edit/${this.recipeId}`)}
)
}
}

View File

@ -0,0 +1,75 @@
import {Injectable} from '@angular/core';
import {ApiService} from "../../shared/service/api.service";
import {convertMixMaterialQuantity, UNIT_MILLILITER} from "../../shared/units";
import {Observable} from "rxjs";
import {Mix} from "../../shared/model/recipe.model";
@Injectable({
providedIn: 'root'
})
export class MixService {
constructor(
private api: ApiService
) {
}
get all(): Observable<Mix[]> {
return this.api.get<Mix[]>('/recipe/mix')
}
getById(id: number): Observable<Mix> {
return this.api.get<Mix>(`/recipe/mix/${id}`)
}
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: { materialId: number, quantity: number, percents: boolean }[], units: string): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
}
save(name: string, recipeId: number, materialTypeId: number, mixMaterials: { materialId: number, quantity: number }[]): Observable<void> {
const body = {
name,
recipeId,
materialTypeId,
mixMaterials: {}
}
this.appendMixMaterialsToBody(mixMaterials, body)
return this.api.post('/recipe/mix', body)
}
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: { materialId: number, quantity: number, percents: boolean }[], units: string): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
}
update(id: number, name: string, materialTypeId: number, mixMaterials: { materialId: number, quantity: number }[]): Observable<void> {
const body = {
id,
name,
materialTypeId,
mixMaterials: {}
}
this.appendMixMaterialsToBody(mixMaterials, body)
return this.api.put('/recipe/mix', body)
}
extractMixMaterials(mix: Mix): { materialId: number, quantity: number, percents: boolean }[] {
return mix.mixMaterials.map(m => {
return {materialId: m.material.id, quantity: m.quantity, percents: m.material.materialType.usePercentages}
})
}
private convertMixMaterialsToMl(mixMaterials: { materialId: number, quantity: number, percents: boolean }[], units: string): { materialId: number, quantity: number }[] {
return mixMaterials.map(m => {
return {
materialId: m.materialId,
quantity: convertMixMaterialQuantity(m, units, UNIT_MILLILITER)
}
})
}
private appendMixMaterialsToBody(mixMaterials: { materialId: number, quantity: number }[], body: any) {
mixMaterials
.filter(m => m.materialId != null && m.quantity != null)
.forEach(m => body.mixMaterials[m.materialId] = m.quantity)
}
}

View File

@ -1,9 +1,106 @@
import { Injectable } from '@angular/core';
import {Injectable} from '@angular/core';
import {ApiService} from "../../shared/service/api.service";
import {Observable} from "rxjs";
import {Recipe, RecipeStep} from "../../shared/model/recipe.model";
import {map} from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class RecipeService {
constructor(
private api: ApiService
) {
}
constructor() { }
get all(): Observable<Recipe[]> {
return this.api.get<Recipe[]>('/recipe')
}
get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> {
return this.all.pipe(map(recipes => {
const mapped = []
recipes.forEach(r => {
if (!mapped[r.company.id]) {
mapped[r.company.id] = {company: r.company.name, recipes: []}
}
mapped[r.company.id].recipes.push(r)
})
return mapped.filter(e => e != null) // Filter to remove empty elements in the array that appears for some reason
}))
}
getById(id: number): Observable<Recipe> {
return this.api.get<Recipe>(`/recipe/${id}`)
}
save(name: string, description: string, sample: number, approbationDate: string, remark: string, companyId: number): Observable<Recipe> {
const body = {name, description, sample, remark, companyId}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
}
return this.api.post<Recipe>('/recipe', body)
}
update(id: number, name: string, description: string, sample: number, approbationDate: string, remark: string, steps: RecipeStep[] = null) {
const body = {id, name, description, sample, remark, steps}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
}
return this.api.put<Recipe>('/recipe', body)
}
saveExplorerModifications(id: number, note: string, mixesLocationChange: Map<number, string>): Observable<void> {
const body = {
id,
note,
mixesLocation: {}
}
mixesLocationChange.forEach((l, i) => body.mixesLocation[i] = l)
return this.api.put<void>('/recipe/public', body)
}
deductMixQuantities(recipe: Recipe, mixId: number, quantities: Map<number, number>): Observable<void> {
return this.sendDeductBody(this.buildDeductMixBody(recipe, mixId, quantities))
}
deductQuantities(recipe: Recipe, quantities: Map<number, Map<number, number>>): Observable<void> {
return this.sendDeductBody(this.buildDeductBody(recipe, quantities))
}
delete(id: number): Observable<void> {
return this.api.delete<void>(`/recipe/${id}`)
}
private buildDeductMixBody(recipe: Recipe, mixId: number, quantities: Map<number, number>): any {
const mix = recipe.mixes.filter(m => m.id === mixId)[0]
const body = {id: recipe.id, quantities: {}}
body.quantities[mixId] = {}
const firstMaterial = mix.mixMaterials[0].material.id
mix.mixMaterials.forEach(m => {
if (quantities && quantities.has(m.material.id)) {
body.quantities[mix.id][m.material.id] = quantities.get(m.material.id)
} else {
let quantity = m.quantity
if (m.material.materialType.usePercentages) quantity = body.quantities[mix.id][firstMaterial] * (quantity / 100)
body.quantities[mix.id][m.material.id] = quantity
}
})
return body
}
private buildDeductBody(recipe: Recipe, quantities: Map<number, Map<number, number>>): any {
const body = {id: recipe.id, quantities: {}}
recipe.mixes.forEach(mix => {
body.quantities[mix.id] = this.buildDeductMixBody(recipe, mix.id, quantities.get(mix.id)).quantities[mix.id]
})
return body
}
private sendDeductBody(body: any): Observable<void> {
return this.api.put('/recipe/deduct', body)
}
}

View File

@ -1,9 +1,9 @@
import {Component} from '@angular/core';
import {CompanyService} from "../../service/company.service";
import {Validators} from "@angular/forms";
import {FormBuilder, Validators} from "@angular/forms";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-add',
@ -28,9 +28,10 @@ export class AddComponent extends SubscribingComponent {
constructor(
private companyService: CompanyService,
private router: Router
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
submit(values) {
@ -39,12 +40,12 @@ export class AddComponent extends SubscribingComponent {
{
next: () => this.router.navigate(['/catalog/company/list']),
error: err => {
if (err.status == 409) {
if (err.status === 409) {
this.errorMessage = `Une bannière avec le nom '${values.name}' existe déjà`
} else {
this.unknownError = true
console.log(err)
}
console.log(err)
}
}
)

View File

@ -2,7 +2,7 @@ import {Component} from '@angular/core';
import {SubscribingComponent} 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 {FormBuilder, Validators} from "@angular/forms";
import {CompanyService} from "../../service/company.service";
import {ActivatedRoute, Router} from "@angular/router";
@ -30,10 +30,10 @@ export class EditComponent extends SubscribingComponent {
constructor(
private companyService: CompanyService,
private router: Router,
private activatedRoute: ActivatedRoute
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -2,6 +2,8 @@ import {Component} from '@angular/core';
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {CompanyService} from "../../service/company.service";
import {EmployeePermission} from "../../../shared/model/employee";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-list',
@ -20,8 +22,10 @@ export class ListComponent extends SubscribingComponent {
}]
constructor(
private companyService: CompanyService
private companyService: CompanyService,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
}

View File

@ -0,0 +1,2 @@
mat-card
max-width: 90rem

View File

@ -1,12 +1,12 @@
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
import {EmployeeGroup} from "../../../shared/model/employee";
import {Observable, Subject} from "rxjs";
import {GroupService} from "../../../groups/services/group.service";
import {take, takeUntil} from "rxjs/operators";
import {EmployeeService} from "../../services/employee.service";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
@Component({
@ -30,9 +30,10 @@ export class AddComponent extends SubscribingComponent {
constructor(
private employeeService: EmployeeService,
private groupService: GroupService,
private router: Router
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -0,0 +1,2 @@
mat-card
max-width: 90rem

View File

@ -29,11 +29,11 @@ export class EditComponent extends SubscribingComponent {
private accountService: AccountService,
private employeeService: EmployeeService,
private groupService: GroupService,
private activatedRoute: ActivatedRoute,
private router: Router,
private formBuilder: FormBuilder
private formBuilder: FormBuilder,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -6,6 +6,8 @@ import {takeUntil} from "rxjs/operators";
import {AccountService} from "../../../accounts/services/account.service";
import {animate, state, style, transition, trigger} from "@angular/animations";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-employees',
@ -27,9 +29,11 @@ export class ListComponent extends SubscribingComponent {
constructor(
private employeeService: EmployeeService,
private accountService: AccountService
private accountService: AccountService,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -19,10 +19,10 @@ export class PasswordEditComponent extends SubscribingComponent {
constructor(
private employeeService: EmployeeService,
private formBuilder: FormBuilder,
private router: Router,
private activatedRoute: ActivatedRoute
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -1,5 +1,5 @@
mat-card
width: max-content
max-width: 90rem
mat-checkbox
font-size: .8em

View File

@ -1,7 +1,7 @@
import {Component, ViewChild} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {GroupService} from "../../services/group.service";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
@ -20,9 +20,10 @@ export class AddComponent extends SubscribingComponent {
constructor(
private formBuilder: FormBuilder,
private groupService: GroupService,
private router: Router
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -0,0 +1,2 @@
mat-card
max-width: 90rem

View File

@ -21,13 +21,13 @@ export class EditComponent extends SubscribingComponent {
private _nameControl: FormControl
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private accountService: AccountService,
private groupService: GroupService,
private formBuilder: FormBuilder
private formBuilder: FormBuilder,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -6,6 +6,8 @@ import {takeUntil} from "rxjs/operators";
import {animate, state, style, transition, trigger} from "@angular/animations";
import {AccountService} from "../../../accounts/services/account.service";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-groups',
@ -27,9 +29,11 @@ export class ListComponent extends SubscribingComponent {
constructor(
private groupService: GroupService,
private accountService: AccountService
private accountService: AccountService,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {

View File

@ -1,9 +1,9 @@
import {Component} from '@angular/core';
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {Validators} from "@angular/forms";
import {FormBuilder, Validators} from "@angular/forms";
import {MaterialTypeService} from "../../service/material-type.service";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-add',
@ -48,9 +48,10 @@ export class AddComponent extends SubscribingComponent {
constructor(
private materialTypeService: MaterialTypeService,
private router: Router
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
submit(values) {

View File

@ -4,7 +4,7 @@ import {ActivatedRoute, Router} from "@angular/router";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {MaterialTypeService} from "../../service/material-type.service";
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {Validators} from "@angular/forms";
import {FormBuilder, Validators} from "@angular/forms";
@Component({
selector: 'cre-edit',
@ -44,10 +44,10 @@ export class EditComponent extends SubscribingComponent {
constructor(
private materialTypeService: MaterialTypeService,
private router: Router,
private activatedRoute: ActivatedRoute
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit() {

View File

@ -2,6 +2,8 @@ import {Component} from '@angular/core';
import {MaterialTypeService} from "../../service/material-type.service";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {EmployeePermission} from "../../../shared/model/employee";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-list',
@ -25,8 +27,10 @@ export class ListComponent extends SubscribingComponent {
]
constructor(
private materialTypeService: MaterialTypeService
private materialTypeService: MaterialTypeService,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
}

View File

@ -1,9 +1,9 @@
import {Component} from '@angular/core';
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {Validators} from "@angular/forms";
import {FormBuilder, Validators} from "@angular/forms";
import {MaterialService} from "../../service/material.service";
import {MaterialTypeService} from "../../../material-type/service/material-type.service";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {map} from "rxjs/operators";
@ -63,9 +63,10 @@ export class AddComponent extends SubscribingComponent {
constructor(
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
private router: Router
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
submit(values) {

View File

@ -1,6 +1,6 @@
import {Component, ViewChild} from '@angular/core';
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {Validators} from "@angular/forms";
import {FormBuilder, Validators} from "@angular/forms";
import {map} from "rxjs/operators";
import {MaterialTypeService} from "../../../material-type/service/material-type.service";
import {MaterialService} from "../../service/material.service";
@ -71,10 +71,10 @@ export class EditComponent extends SubscribingComponent {
constructor(
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
private router: Router,
private activatedRoute: ActivatedRoute,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit() {
@ -114,8 +114,8 @@ export class EditComponent extends SubscribingComponent {
this.errorMessage = `Un produit avec le nom '${values.name}' existe déjà`
} else {
this.unknownError = true
console.error(err)
}
console.log(err)
}
}
)
@ -128,7 +128,7 @@ export class EditComponent extends SubscribingComponent {
next: () => this.router.navigate(['/catalog/material/list']),
error: err => {
this.unknownError = true
console.log(err)
console.error(err)
}
}
)

View File

@ -3,6 +3,8 @@ import {SubscribingComponent} from "../../../shared/components/subscribing.compo
import {MaterialService} from "../../service/material.service";
import {EmployeePermission} from "../../../shared/model/employee";
import {environment} from "../../../../../environments/environment";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-list',
@ -39,9 +41,11 @@ export class ListComponent extends SubscribingComponent {
private hasSimdutMap: any = {}
constructor(
private materialService: MaterialService
private materialService: MaterialService,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit() {

View File

@ -17,6 +17,14 @@ export class MaterialService {
return this.api.get<Material[]>('/material')
}
getAllForMixCreation(recipeId: number): Observable<Material[]> {
return this.api.get<Material[]>(`/material/mix/create/${recipeId}`)
}
getAllForMixUpdate(mixId: number): Observable<Material[]> {
return this.api.get<Material[]>(`/material/mix/update/${mixId}`)
}
getById(id: number): Observable<Material> {
return this.api.get<Material>(`/material/${id}`)
}

View File

@ -2,6 +2,7 @@ import {Component, EventEmitter, Input, Output} from '@angular/core';
import {SubscribingComponent} from "../subscribing.component";
import {FormBuilder, FormControl, FormGroup, ValidatorFn} from "@angular/forms";
import {Observable} from "rxjs";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-entity-add',
@ -19,9 +20,11 @@ export class EntityAddComponent extends SubscribingComponent {
form: FormGroup | null
constructor(
private formBuilder: FormBuilder
private formBuilder: FormBuilder,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit() {
@ -45,10 +48,6 @@ export class EntityAddComponent extends SubscribingComponent {
getControl(controlName: string): FormControl {
return this.form.controls[controlName] as FormControl
}
test(any) {
console.log(any)
}
}
export class FormField {
@ -61,6 +60,7 @@ export class FormField {
public errorMessages?: FormErrorMessage[],
public valueFn?: (any) => any,
public template?: any,
public readonly?: boolean,
// Specifics to some types
public step?: string,
public options$?: Observable<{ value: any, label: string }[]>,

View File

@ -2,7 +2,7 @@
<mat-card-header>
<mat-card-title>{{title}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-card-content [class.no-action]="disableButtons">
<div *ngIf="unknownError || customError" class="alert alert-danger">
<p *ngIf="unknownError">Une erreur est survenue</p>
<p *ngIf="customError">{{customError}}</p>
@ -32,10 +32,12 @@
</ng-container>
</form>
</mat-card-content>
<mat-card-actions>
<mat-card-actions *ngIf="!disableButtons">
<button mat-raised-button color="primary" [routerLink]="backButtonLink">Retour</button>
<button mat-raised-button color="warn" *ngIf="canDelete" (click)="confirmBoxComponent.show()">Supprimer</button>
<button mat-raised-button color="accent" [disabled]="form.invalid" (click)="submitForm()">Enregistrer</button>
<button mat-raised-button color="accent" [disabled]="form.invalid" (click)="submitForm()">
Enregistrer
</button>
</mat-card-actions>
</mat-card>
@ -44,7 +46,7 @@
let-control="control" let-field="field">
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<input matInput [type]="field.type" [formControl]="control"/>
<input matInput [type]="field.type" [formControl]="control" [readonly]="field.readonly"/>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
<mat-error *ngIf="control.invalid && field.errorMessages">
<ng-container *ngFor="let errorMessage of field.errorMessages">
@ -59,7 +61,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" [disabled]="field.readonly">
<mat-option *ngFor="let option of options" [value]="option.value">
{{option.label}}
</mat-option>

View File

@ -4,6 +4,7 @@ import {SubscribingComponent} from "../subscribing.component";
import {FormField} from "../entity-add/entity-add.component";
import {EmployeePermission} from "../../model/employee";
import {AccountService} from "../../../accounts/services/account.service";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-entity-edit',
@ -19,6 +20,7 @@ export class EntityEditComponent extends SubscribingComponent {
@Input() deletePermission: EmployeePermission
@Input() unknownError = false
@Input() customError: string | null
@Input() disableButtons: boolean
@Output() submit = new EventEmitter<any>()
@Output() delete = new EventEmitter<void>()
@ -26,9 +28,11 @@ export class EntityEditComponent extends SubscribingComponent {
constructor(
private accountService: AccountService,
private formBuilder: FormBuilder
private formBuilder: FormBuilder,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit() {
@ -42,17 +46,21 @@ export class EntityEditComponent extends SubscribingComponent {
}
submitForm() {
const values = {}
this.formFields.forEach(f => {
values[f.name] = this.getControl(f.name).value
})
this.submit.emit(values)
this.submit.emit(this.values)
}
getControl(controlName: string): FormControl {
return this.form.controls[controlName] as FormControl
}
get values(): any {
const values = {}
this.formFields.forEach(f => {
values[f.name] = this.getControl(f.name).value
})
return values
}
get canDelete(): boolean {
return this.accountService.hasPermission(this.deletePermission)
}

View File

@ -3,6 +3,8 @@ import {Observable} from "rxjs";
import {SubscribingComponent} from "../subscribing.component";
import {AccountService} from "../../../accounts/services/account.service";
import {EmployeePermission} from "../../model/employee";
import {FormBuilder} from "@angular/forms";
import {ActivatedRoute, Router} from "@angular/router";
@Component({
selector: 'cre-entity-list',
@ -18,9 +20,11 @@ export class EntityListComponent<T> extends SubscribingComponent {
@Input() addPermission: EmployeePermission
constructor(
private accountService: AccountService
private accountService: AccountService,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
hasPermissionToUseButton(button: TableButton): boolean {

View File

@ -1,9 +1,10 @@
import {Component} from '@angular/core';
import {ResolveEnd, Router} from "@angular/router";
import {ActivatedRoute, ResolveEnd, Router} from "@angular/router";
import {AppState} from "../../app-state";
import {Employee, EmployeePermission} from "../../model/employee";
import {AccountService} from "../../../accounts/services/account.service";
import {SubscribingComponent} from "../subscribing.component";
import {FormBuilder} from "@angular/forms";
@Component({
selector: 'cre-header',
@ -12,9 +13,10 @@ import {SubscribingComponent} from "../subscribing.component";
})
export class HeaderComponent extends SubscribingComponent {
links: HeaderLink[] = [
{route: '/color', title: 'Couleurs', requiredPermission: EmployeePermission.VIEW_RECIPE},
{route: '/catalog', title: 'Catalogue', enabled: true},
new HeaderLink('/employee', 'Employés', EmployeePermission.VIEW_EMPLOYEE),
new HeaderLink('/group', 'Groupes', EmployeePermission.VIEW_EMPLOYEE_GROUP),
{route: '/employee', title: 'Employés', requiredPermission: EmployeePermission.VIEW_EMPLOYEE},
{route: '/group', title: 'Groupes', requiredPermission: EmployeePermission.VIEW_EMPLOYEE_GROUP},
{route: '/account/login', title: 'Connexion', enabled: true},
{route: '/account/logout', title: 'Déconnexion', enabled: false},
];
@ -22,10 +24,11 @@ export class HeaderComponent extends SubscribingComponent {
constructor(
private accountService: AccountService,
private router: Router,
private appState: AppState
private appState: AppState,
router: Router,
activatedRoute: ActivatedRoute
) {
super()
super(activatedRoute, router)
}
ngOnInit(): void {
@ -89,7 +92,7 @@ class HeaderLink {
public route: string,
public title: string,
public requiredPermission?: EmployeePermission,
public enabled = false
public enabled?
) {
}
}

View File

@ -1,14 +1,24 @@
import {take, takeUntil} from "rxjs/operators";
import {OnDestroy, OnInit} from "@angular/core";
import {Observable, Subject} from "rxjs";
import {ActivatedRoute, Router} from "@angular/router";
import {UrlUtils} from "../utils/url.utils";
export abstract class SubscribingComponent implements OnInit, OnDestroy {
protected subscribers$ = []
protected destroy$ = new Subject<boolean>()
unknownError = false
protected constructor(
protected activatedRoute: ActivatedRoute,
protected router: Router,
protected urlUtils = new UrlUtils(activatedRoute, router)
) {
}
subscribe<T>(observable: Observable<T>, observer, take_count = -1) {
if (!observer.error) {
observer.error = err => console.log(err)
observer.error = err => this.handleObserverError(err)
}
if (take_count >= 0) {
@ -19,6 +29,24 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy {
this.subscribers$.push(observable.subscribe(observer))
}
subscribeEntityByIdFromRoute<T>(service: any, entitySubject: Subject<T>, notFoundRoute: string, paramName: string = 'id') {
const id = this.urlUtils.parseIntUrlParam(paramName)
this.subscribe(
service.getById(id),
{
next: e => entitySubject.next(e),
error: err => {
if (err.status === 404) {
this.urlUtils.navigateTo(notFoundRoute)
} else {
this.handleObserverError(err)
}
}
},
1
)
}
ngOnInit(): void {
}
@ -26,4 +54,17 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy {
this.destroy$.next(true)
this.destroy$.complete()
}
handleObserverError(error: any) {
console.error(error)
this.unknownError = true
}
handleNotFoundError(error: any, route: string) {
if (error.status === 404) {
this.urlUtils.navigateTo(route)
} else {
this.handleObserverError(error)
}
}
}

View File

@ -0,0 +1,55 @@
import {Material} from "./material.model";
import {LocalDate} from "js-joda";
import {Company} from "./company.model";
export class Recipe {
constructor(
public id: number,
public name: string,
public description: string,
public sample: number,
public approbationDate: LocalDate,
public remark: string,
public note: string,
public company: Company,
public mixes: Mix[],
public steps: RecipeStep[]
) {
}
}
export class Mix {
constructor(
public id: number,
public mixType: MixType,
public mixMaterials: MixMaterial[],
public location: string,
) {
}
}
export class MixMaterial {
constructor(
public id: number,
public material: Material,
public quantity: number
) {
}
}
class MixType {
constructor(
public id: number,
public name: string,
public material: Material
) {
}
}
export class RecipeStep {
constructor(
public id: number,
public message: string
) {
}
}

View File

@ -0,0 +1,32 @@
export const UNIT_MILLILITER = 'mL'
export const UNIT_LITER = 'L'
export const UNIT_GALLON = 'gal'
export const LITER_TO_MILLILITER = 1000
export const GALLON_TO_MILLILITER = 3785.411784
export const UNIT_RATIOS = {
mL: {
mL: 1,
L: 1 / LITER_TO_MILLILITER,
gal: 1 / GALLON_TO_MILLILITER
},
L: {
mL: LITER_TO_MILLILITER,
L: 1,
gal: LITER_TO_MILLILITER / GALLON_TO_MILLILITER
},
gal: {
mL: GALLON_TO_MILLILITER,
L: 1 / (LITER_TO_MILLILITER / GALLON_TO_MILLILITER),
gal: 1
}
}
export function convertMixMaterialQuantity(computedQuantity: { percents: boolean, quantity: number }, from: string, to: string): number {
return !computedQuantity.percents ? convertQuantity(computedQuantity.quantity, from, to) : computedQuantity.quantity
}
export function convertQuantity(quantity: number, from: string, to: string): number {
return quantity * UNIT_RATIOS[from][to]
}

View File

@ -0,0 +1,21 @@
import {ActivatedRoute, Router} from "@angular/router";
export class UrlUtils {
constructor(
private activatedRoute: ActivatedRoute,
private router: Router
) {
}
parseUrlParam(param: string): string {
return this.activatedRoute.snapshot.paramMap.get(param)
}
parseIntUrlParam(param: string): number {
return parseInt(this.parseUrlParam(param))
}
navigateTo(url: string) {
this.router.navigate([url])
}
}

View File

@ -92,6 +92,7 @@ $color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-a
$color-primary: map-get($theme-primary, 500);
$color-accent: map-get($theme-accent, 500);
$color-warn: map-get($theme-warn, 500);
html, body {

View File

@ -1,8 +1,10 @@
@import "custom-theme"
@import "~material-design-icons/iconfont/material-icons.css"
mat-card
padding: 0 !important
width: max-content
max-width: 50rem
&.x-centered
margin: auto
@ -22,6 +24,9 @@ mat-card
margin-top: 16px
padding: 0 16px
&.no-action
padding: 0 24px 16px 24px !important
mat-form-field
width: 100%
@ -38,6 +43,7 @@ mat-card
table
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12)
max-width: 90vw
th
background-color: $color-primary
@ -72,12 +78,62 @@ table
.disabled *
display: none
mat-expansion-panel.table-title
mat-expansion-panel-header
background-color: $color-primary
&:hover, &:focus
background-color: $color-primary !important
mat-panel-title
color: $light-primary-text !important
font-weight: bold
text-transform: uppercase
mat-panel-description
color: $light-primary-text
mat-form-field
display: inline
&.mat-expanded mat-expansion-panel-header
border-bottom-left-radius: 0
border-bottom-right-radius: 0
.mat-expansion-panel-body
padding: 0 !important
table
width: 100%
th
border-top-left-radius: 0
border-top-right-radius: 0
cre-mix-table.no-top-margin mat-expansion-panel
margin-top: 0 !important
button
text-transform: uppercase
font-weight: 500
&.mat-accent
color: white !important
color: $light-primary-text !important
mat-form-field
&.w-auto
.mat-form-field-infix
width: auto !important
&.dark
input
caret-color: $light-primary-text !important
mat-label, input
color: $light-primary-text
.mat-form-field-underline, .mat-form-field-ripple
background-color: $light-primary-text !important
div.empty
color: $dark-secondary-text
@ -92,6 +148,13 @@ div.empty
button
margin-left: 1rem
&.backward
justify-content: flex-start
button
margin-left: 0
margin-right: 1rem
.alert p
margin-bottom: 0
@ -112,3 +175,6 @@ div.empty
left: 0
background-color: black
opacity: 0.4
.color-warning
color: #fdd835

View File

@ -60,7 +60,7 @@ open class MixUpdateDto(
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
val name: String?,
val materialType: MaterialType?,
val materialTypeId: Long?,
val mixMaterials: Map<Long, Float>?
) : EntityDto<Mix> {
@ -88,7 +88,7 @@ fun mixSaveDto(
fun mixUpdateDto(
id: Long = 0L,
name: String? = "name",
materialType: MaterialType? = materialType(),
materialTypeId: Long? = 0L,
mixMaterials: Map<Long, Float>? = mapOf(),
op: MixUpdateDto.() -> Unit = {}
) = MixUpdateDto(id, name, materialType, mixMaterials).apply(op)
) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op)

View File

@ -55,17 +55,17 @@ class MixServiceImpl(
override fun update(entity: MixUpdateDto): Mix {
val mix = getById(entity.id)
if (entity.name != null || entity.materialType != null) {
if (entity.name != null || entity.materialTypeId != null) {
mix.mixType = if (mixTypeIsShared(mix.mixType)) {
mixTypeService.createForNameAndMaterialType(
entity.name ?: mix.mixType.name,
entity.materialType ?: mix.mixType.material.materialType!!
if (entity.materialTypeId != null) materialTypeService.getById(entity.materialTypeId) else mix.mixType.material.materialType!!
)
} else {
mixTypeService.updateForNameAndMaterialType(
mix.mixType,
entity.name ?: mix.mixType.name,
entity.materialType ?: mix.mixType.material.materialType!!
if (entity.materialTypeId != null) materialTypeService.getById(entity.materialTypeId) else mix.mixType.material.materialType!!
)
}
}

View File

@ -272,7 +272,7 @@
let i = 0;
$(".mixContainer").each(function () {
formData[i++] = getMixQuantities($(this));
formData[i++] = getMixMaterials($(this));
});
clearNotEnoughClasses();
@ -284,7 +284,7 @@
$(".useMixSubmit").on({
click: function () {
showConfirm(formatMessage("[[#{inventory.askUseMix}]]"), false, () => {
let formData = [getMixQuantities($(this).parents(".mixContainer"))];
let formData = [getMixMaterials($(this).parents(".mixContainer"))];
clearNotEnoughClasses();
sendPost(formData, "/inventory/use", r => displayNotEnoughReason(r));
@ -384,7 +384,7 @@
});
}
function getMixQuantities(mixContainer) {
function getMixMaterials(mixContainer) {
mix = $(mixContainer).find(".mix");
const mixId = mix.data("mixid");