Merge pull request 'Several updates' (#3) from 14-no-entity-message into develop
continuous-integration/drone/push Build is passing Details

Reviewed-on: #3
This commit is contained in:
William Nolin 2021-12-02 15:00:52 -05:00
commit 23b80daa75
133 changed files with 2186 additions and 1584 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ speed-measure-plugin*.json
.history/* .history/*
# misc # misc
/.angular/cache
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage

View File

@ -22,7 +22,6 @@
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets", "src/assets",
@ -37,7 +36,13 @@
"src/custom-theme.scss", "src/custom-theme.scss",
"src/styles.sass" "src/styles.sass"
], ],
"scripts": [] "scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": false,
"optimization": false,
"namedChunks": true
}, },
"configurations": { "configurations": {
"production": { "production": {
@ -67,7 +72,8 @@
} }
] ]
} }
} },
"defaultConfiguration": ""
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",

View File

@ -1,19 +1,11 @@
version: "3.1" version: "3.1"
services: services:
database: cre.backend:
image: mysql
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- "3306:3306"
backend:
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment: environment:
spring_profiles_active: "mysql,debug" spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database:3306/cre" cre_database_url: "mysql://database/cre"
cre_database_username: "root" cre_database_username: "root"
cre_database_password: "pass" cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 1 CRE_ENABLE_DB_UPDATE: 1
@ -23,6 +15,14 @@ services:
volumes: volumes:
- cre_data:/usr/bin/cre/data - cre_data:/usr/bin/cre/data
- cre_config:/usr/bin/cre/config - cre_config:/usr/bin/cre/config
cre.database:
image: mysql
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- "3307:3306"
volumes: volumes:
cre_data: cre_data:

View File

@ -11,54 +11,54 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~11.2.10", "@angular/animations": "~12.2.14",
"@angular/cdk": "^11.2.11", "@angular/cdk": "^12.2.13",
"@angular/common": "~11.2.10", "@angular/common": "~12.2.14",
"@angular/compiler": "~11.2.10", "@angular/compiler": "~12.2.14",
"@angular/core": "~11.2.10", "@angular/core": "~12.2.14",
"@angular/forms": "~11.2.10", "@angular/forms": "~12.2.14",
"@angular/material": "^11.2.9", "@angular/material": "^12.2.13",
"@angular/platform-browser": "~11.2.10", "@angular/platform-browser": "~12.2.14",
"@angular/platform-browser-dynamic": "~11.2.10", "@angular/platform-browser-dynamic": "~12.2.14",
"@angular/router": "~11.2.10", "@angular/router": "~12.2.14",
"@mdi/angular-material": "^5.7.55", "@mdi/angular-material": "^6.5.95",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"copy-webpack-plugin": "^6.2.1", "copy-webpack-plugin": "^10.0.0",
"js-joda": "^1.11.0", "@js-joda/core": "^4.3.1",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"ngx-material-file-input": "^2.1.1", "ngx-material-file-input": "^2.1.1",
"rxjs": "~6.5.4", "rxjs": "^7.4.0",
"tslib": "^2.0.0", "tslib": "^2.3.1",
"zone.js": "~0.10.2" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.1102.9", "@angular-devkit/build-angular": "^12.2.13",
"@angular-eslint/builder": "4.3.0", "@angular-eslint/builder": "4.3.0",
"@angular-eslint/eslint-plugin": "4.3.0", "@angular-eslint/eslint-plugin": "4.3.0",
"@angular-eslint/eslint-plugin-template": "4.3.0", "@angular-eslint/eslint-plugin-template": "4.3.0",
"@angular-eslint/schematics": "4.3.0", "@angular-eslint/schematics": "4.3.0",
"@angular-eslint/template-parser": "4.3.0", "@angular-eslint/template-parser": "4.3.0",
"@angular/cli": "^11.2.11", "@angular/cli": "^12.2.13",
"@angular/compiler-cli": "~11.2.10", "@angular/compiler-cli": "~12.2.14",
"@angular/language-service": "~11.2.10", "@angular/language-service": "~12.2.14",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "4.16.1", "@typescript-eslint/eslint-plugin": "4.16.1",
"@typescript-eslint/parser": "4.16.1", "@typescript-eslint/parser": "4.16.1",
"eslint": "^7.6.0", "eslint": "^8.3.0",
"eslint-plugin-import": "latest", "eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest", "eslint-plugin-jsdoc": "latest",
"eslint-plugin-prefer-arrow": "latest", "eslint-plugin-prefer-arrow": "latest",
"jasmine-core": "~3.6.0", "jasmine-core": "^3.10.1",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "^7.0.0",
"karma": "~6.3.2", "karma": "~6.3.2",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"ts-node": "~8.3.0", "ts-node": "^10.4.0",
"typescript": "~4.0.7" "typescript": "~4.3.5"
} }
} }

View File

@ -18,3 +18,7 @@ $text-color-primary: white;
$color-accent: map-get($theme-accent, 500); $color-accent: map-get($theme-accent, 500);
$color-warn: map-get($theme-error, 500); $color-warn: map-get($theme-error, 500);
$light-primary-text: white;
$dark-primary-text: black;
$dark-secondary-text: black;

View File

@ -7,7 +7,7 @@ import {CreConfigEditor} from './modules/configuration/config-editor'
const routes: Routes = [{ const routes: Routes = [{
path: 'color', path: 'color',
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule) loadChildren: () => import('./modules/recipes/recipes.module').then(m => m.RecipesModule)
}, { }, {
path: 'account', path: 'account',
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule) loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)

View File

@ -5,7 +5,6 @@ import {SubscribingComponent} from './modules/shared/components/subscribing.comp
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from './modules/shared/service/error.service' import {ErrorService} from './modules/shared/service/error.service'
import {ConfigService} from './modules/shared/service/config.service' import {ConfigService} from './modules/shared/service/config.service'
import {Config} from './modules/shared/model/config.model'
import {environment} from '../environments/environment' import {environment} from '../environments/environment'
@Component({ @Component({
@ -38,7 +37,7 @@ export class AppComponent extends SubscribingComponent {
online => this.isServerOnline = online online => this.isServerOnline = online
) )
this.favIcon.href = environment.apiUrl + "/file?path=images%2Ficon" this.favIcon.href = environment.apiUrl + "/config/icon"
} }
reload() { reload() {

View File

@ -1,39 +0,0 @@
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";
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 {
}

View File

@ -1,38 +0,0 @@
import {NgModule} from '@angular/core'
import {ColorsRoutingModule} from './colors-routing.module'
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'
import {ImagesEditorComponent} from './components/images-editor/images-editor.component'
import {MixesCardComponent} from './components/mixes-card/mixes-card.component'
import {MatSortModule} from '@angular/material/sort'
@NgModule({
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent, ImagesEditorComponent, MixesCardComponent],
exports: [
UnitSelectorComponent
],
imports: [
ColorsRoutingModule,
SharedModule,
MatExpansionModule,
FormsModule,
MatSortModule
]
})
export class ColorsModule {
}

View File

@ -1,136 +0,0 @@
<mat-card *ngIf="recipe && (!editionMode || mix)" class="x-centered mt-5">
<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>
<div class="mix-materials-wrapper">
<ng-container *ngTemplateOutlet="mixEditor"></ng-container>
</div>
</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)="deleteConfirmBox.show()">
Supprimer
</button>
<button mat-raised-button color="accent" [disabled]="!form.valid" (click)="submit()">Enregistrer</button>
</mat-card-actions>
</mat-card>
<ng-template #mixEditor>
<table #matTable mat-table [dataSource]="mixMaterials">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let mixMaterial">
{{mixMaterial.position}}
</td>
</ng-container>
<ng-container matColumnDef="buttonsPosition">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mixMaterial; let i = index">
<ng-container *ngIf="(!hoveredMixMaterial && i === 0) || hoveredMixMaterial === mixMaterial">
<button
mat-mini-fab
color="primary"
class="mr-1"
[disabled]="mixMaterial.position <= 1"
(click)="decreasePosition(mixMaterial, matTable)">
<mat-icon svgIcon="arrow-up"></mat-icon>
</button>
<button
mat-mini-fab
color="primary"
[disabled]="mixMaterial.position >= mixMaterials.length"
(click)="increasePosition(mixMaterial, matTable)">
<mat-icon svgIcon="arrow-down"></mat-icon>
</button>
</ng-container>
</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
[value]="mixMaterial.materialId"
(valueChange)="setMixMaterialMaterial(mixMaterial, $event)">
<mat-option
*ngFor="let material of sortedMaterials(getAvailableMaterials(mixMaterial))"
[value]="material.id">
{{materialDisplayName(material)}}
</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="mixMaterial.isPercents">
<p>%</p>
</ng-container>
<ng-container *ngIf="!mixMaterial.isPercents">
<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>
<cre-confirm-box
*ngIf="editionMode && mix"
#deleteConfirmBox
message="Voulez-vous vraiment supprimer le mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}}"
(confirm)="delete()">
</cre-confirm-box>

View File

@ -1,6 +0,0 @@
mat-card
max-width: unset !important
td.units-wrapper p
width: 3rem
margin-bottom: 0

View File

@ -1,220 +0,0 @@
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
import {
Mix,
MixMaterial,
MixMaterialDto,
mixMaterialsAsMixMaterialsDto,
Recipe,
sortMixMaterialsDto
} from '../../../shared/model/recipe.model'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {MixService} from '../../services/mix.service'
import {RecipeService} from '../../services/recipe.service'
import {Material} from '../../../shared/model/material.model'
import {MaterialService} from '../../../material/service/material.service'
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'
import {ConfirmBoxComponent} from '../../../shared/components/confirm-box/confirm-box.component'
import {AccountService} from '../../../accounts/services/account.service'
import {ErrorService} from '../../../shared/service/error.service'
@Component({
selector: 'cre-mix-editor',
templateUrl: './mix-editor.component.html',
styleUrls: ['./mix-editor.component.sass']
})
export class MixEditorComponent extends ErrorHandlingComponent {
@ViewChild('matTable') mixTable: MatTable<MixMaterial>
@ViewChild('deleteConfirmBox') deleteConfirmBox: ConfirmBoxComponent
@Input() mixId: number | null
@Input() recipeId: number | null
@Input() materials: Material[]
@Output() save = new EventEmitter<any>()
mix: Mix | null
recipe: Recipe | null
materialTypes$ = this.materialTypeService.all
form: FormGroup
nameControl: FormControl
materialTypeControl: FormControl
mixMaterials: MixMaterialDto[] = []
editionMode = false
units = UNIT_MILLILITER
hoveredMixMaterial: MixMaterial | null
columns = ['position', 'buttonsPosition', 'material', 'quantity', 'units', 'buttonRemove']
deleting = false
errorHandlers = [{
filter: error => error.type === 'notfound-mix-id',
consumer: error => this.urlUtils.navigateTo('/color/list')
}, {
filter: error => error.type === 'exists-material-name',
messageProducer: error => `Un produit avec le nom '${error.name}' existe déjà`
}, {
filter: error => error.type === 'cannotdelete-mix',
messageProducer: error => 'Ce mélange est utilisé par un ou plusieurs autres mélanges'
}, {
filter: error => error.type === 'invalid-mixmaterial-first',
messageProducer: error => 'La quantité du premier ingrédient du mélange ne peut pas être exprimée en pourcentage'
}]
constructor(
private mixService: MixService,
private recipeService: RecipeService,
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
private accountService: AccountService,
private formBuilder: FormBuilder,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.mixId = this.urlUtils.parseIntUrlParam('id')
if (this.mixId) {
this.editionMode = true
}
this.subscribeEntityById(
this.recipeService,
this.recipeId,
r => {
this.recipe = r
if (this.editionMode) {
this.mix = this.recipe.mixes.find(m => m.id === this.mixId)
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix)
} else {
this.addBlankMixMaterial()
}
this.generateForm()
}
)
}
addRow() {
this.addBlankMixMaterial()
this.mixTable.renderRows()
}
removeRow(position: number) {
this.mixMaterials.splice(position, 1)
// Decreases the position of each mix material above the removed one
for (let i = position; i < this.mixMaterials.length; i++) {
this.mixMaterials[i].position -= 1
}
this.mixTable.renderRows()
}
increasePosition(mixMaterial: MixMaterialDto, table: MatTable<any>) {
this.updateMixMaterialPosition(mixMaterial, mixMaterial.position + 1)
this.sort(table)
}
decreasePosition(mixMaterial: MixMaterialDto, table: MatTable<any>) {
this.updateMixMaterialPosition(mixMaterial, mixMaterial.position - 1)
this.sort(table)
}
sort(table: MatTable<any>) {
this.mixMaterials = sortMixMaterialsDto(this.mixMaterials)
table.renderRows()
}
setMixMaterialMaterial(mixMaterial: MixMaterialDto, materialId: number) {
mixMaterial.isPercents = this.materials.find(m => m.id === materialId).materialType.usePercentages
mixMaterial.materialId = materialId
}
submit() {
this.save.emit({
name: this.nameControl.value,
recipeId: this.recipeId,
materialTypeId: this.materialTypeControl.value,
mixMaterials: this.mixMaterials,
units: this.units
})
}
delete() {
this.deleting = true
this.subscribeAndNavigate(this.mixService.delete(this.mixId), `/color/edit/${this.recipeId}`)
}
getAvailableMaterials(mixMaterial: MixMaterialDto): Material[] {
return this.materials.filter(m => mixMaterial.materialId === m.id || this.mixMaterials.filter(mm => mm.materialId === m.id).length === 0)
}
materialDisplayName(material: Material): string {
if (material.materialType.prefix) {
return `[${material.materialType.prefix}] ${material.name}`
}
return material.name
}
sortedMaterials(materials: Material[]): Material[] {
return materials.sort((a, b) => {
const aPrefixName = a.materialType.prefix.toLowerCase()
const bPrefixName = b.materialType.prefix.toLowerCase()
if (aPrefixName < bPrefixName) {
return -1
} else if (aPrefixName > bPrefixName) {
return 1
} else {
const aName = a.name.toLowerCase()
const bName = b.name.toLowerCase()
if (aName < bName) {
return -1
} else if (aName > bName) {
return 1
} else {
return 0
}
}
})
}
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
})
}
private addBlankMixMaterial() {
this.mixMaterials.push(
new MixMaterialDto(null, 0, false, this.mixMaterials.length + 1)
)
}
private updateMixMaterialPosition(mixMaterial: MixMaterialDto, updatedPosition: number) {
if (!this.mixMaterialAtPosition(updatedPosition)) {
mixMaterial.position = updatedPosition
} else {
const conflictingStep = this.mixMaterialAtPosition(updatedPosition)
conflictingStep.position = mixMaterial.position
mixMaterial.position = updatedPosition
}
}
private mixMaterialAtPosition(position: number): MixMaterialDto {
return this.mixMaterials.find(m => m.position === position)
}
}

View File

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

View File

@ -1,123 +0,0 @@
import {Component} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {RecipeService} from '../../services/recipe.service'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
import {Validators} from '@angular/forms'
import {CompanyService} from '../../../company/service/company.service'
import {map} from 'rxjs/operators'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service'
import {AppState} from '../../../shared/app-state'
@Component({
selector: 'cre-add',
templateUrl: './add.component.html',
styleUrls: ['./add.component.sass']
})
export class AddComponent extends ErrorHandlingComponent {
formFields: FormField[] = [
{
name: 'name',
label: 'Nom',
icon: 'form-textbox',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
},
{
name: 'description',
label: 'Description',
icon: 'text',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
},
{
name: 'color',
label: 'Couleur',
icon: 'palette',
type: 'color',
defaultValue: "#ffffff",
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une couleur est requise'}
]
},
{
name: 'gloss',
label: 'Lustre',
type: 'slider',
min: 0,
max: 100,
defaultValue: 0,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Le lustre de la couleur est requis'}
]
},
{
name: 'sample',
label: 'Échantillon',
icon: 'pound',
type: 'number',
validator: 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',
required: true,
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}
})))
}
]
errorHandlers = [{
filter: error => error.type === `exists-recipe-company-name`,
messageProducer: error => `Une couleur avec le nom ${error.name} existe déjà pour la bannière ${error.company}`
}]
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = "Nouvelle couleur"
}
submit(values) {
this.subscribe(
this.recipeService.save(values.name, values.description, values.color, values.gloss, values.sample, values.approbationDate, values.remark, values.company),
recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`)
)
}
}

View File

@ -1,68 +0,0 @@
<div *ngIf="recipe">
<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"
[disabled]="editComponent.form && editComponent.form.invalid"
(click)="submit(editComponent, stepTable)">
Enregistrer
</button>
<button
mat-raised-button
color="warn"
(click)="confirmBoxComponent.show()">
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"
[disableButtons]="true"
[noTopMargin]="true">
</cre-entity-edit>
</div>
<div class="recipe-mixes-wrapper">
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
</div>
<div>
<cre-step-table
#stepTable
[recipe]="recipe"
[groups$]="groups$"
[selectedGroupId]="loggedInUserGroupId">
</cre-step-table>
</div>
<div>
<cre-images-editor #imagesEditor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
</div>
</div>
</div>
<cre-confirm-box
#confirmBoxComponent
message="Voulez-vous vraiment supprimer la couleur {{recipe?.name}}?"
(confirm)="delete()">
</cre-confirm-box>

View File

@ -1,2 +0,0 @@
.recipe-wrapper > div
margin: 0 3rem 3rem

View File

@ -1,186 +0,0 @@
import {Component, ViewChild} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {Recipe, recipeMixCount, RecipeStep, recipeStepCount} from '../../../shared/model/recipe.model'
import {RecipeService} from '../../services/recipe.service'
import {ActivatedRoute, Router} from '@angular/router'
import {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 {EntityEditComponent} from '../../../shared/components/entity-edit/entity-edit.component'
import {ImagesEditorComponent} from '../../components/images-editor/images-editor.component'
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
import {GroupService} from '../../../groups/services/group.service'
import {AppState} from '../../../shared/app-state'
import {StepTableComponent} from '../../components/step-table/step-table.component'
@Component({
selector: 'cre-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.sass']
})
export class EditComponent extends ErrorHandlingComponent {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
@ViewChild('imagesEditor') imagesEditor: ImagesEditorComponent
recipe: Recipe | null
groups$ = this.groupService.all
formFields = [
{
name: 'name',
label: 'Nom',
icon: 'form-textbox',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
},
{
name: 'description',
label: 'Description',
icon: 'text',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
},
{
name: 'color',
label: 'Couleur',
icon: 'palette',
type: 'color',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une couleur est requise'}
]
},
{
name: 'gloss',
label: 'Lustre',
type: 'slider',
min: 0,
max: 100,
validator: Validators.compose([Validators.required, Validators.min(0), Validators.max(100)]),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Le lustre de la couleur est requis'}
]
},
{
name: 'sample',
label: 'Échantillon',
icon: 'pound',
type: 'number',
validator: 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,
}
]
units$ = new Subject<string>()
submittedValues: any | null
errorHandlers: ErrorHandler[] = [{
filter: error => error.type === 'notfound-recipe-id',
consumer: error => this.urlUtils.navigateTo('/color/list')
}]
constructor(
private recipeService: RecipeService,
private groupService: GroupService,
private accountService: AccountService,
private alertService: AlertService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.subscribeEntityById(
this.recipeService,
parseInt(this.activatedRoute.snapshot.paramMap.get('id')),
recipe => {
this.recipe = recipe
this.appState.title = `${recipe.name} (Modifications)`
if (recipeMixCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucun mélange dans cette recette')
}
if (recipeStepCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucune étape dans cette recette')
}
}
)
}
changeUnits(unit: string) {
this.units$.next(unit)
}
submit(editComponent: EntityEditComponent, stepTable: StepTableComponent) {
const values = editComponent.values
this.submittedValues = values
const steps = stepTable.mappedUpdatedSteps
if (!this.stepsPositionsAreValid(steps)) {
this.alertService.pushError('Les étapes ne peuvent pas avoir une position inférieure à 1')
return
}
this.subscribeAndNavigate(
this.recipeService.update(this.recipe.id, values.name, values.description, values.color, values.gloss, values.sample, values.approbationDate, values.remark, steps),
'/color/list'
)
}
delete() {
this.subscribeAndNavigate(
this.recipeService.delete(this.recipe.id),
'/color/list'
)
}
get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id
}
private stepsPositionsAreValid(steps: Map<number, RecipeStep[]>): boolean {
let valid = true
steps.forEach((steps, _) => {
if (steps.find(s => s.position === 0)) {
valid = false
return
}
})
return valid
}
}

View File

@ -1,86 +0,0 @@
<div *ngIf="recipe">
<cre-recipe-info [recipe]="recipe" [hasModifications]="hasModifications"></cre-recipe-info>
<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"
disabled
title="WIP">
Version Excel
</button>
<button
*ngIf="canEditRecipesPublicData"
mat-raised-button
color="accent"
(click)="saveModifications()"
[disabled]="!hasModifications">
Enregistrer
</button>
</div>
<div>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
<mat-form-field class="ml-3">
<mat-label>Groupe</mat-label>
<mat-select [(ngModel)]="selectedGroupId">
<mat-option *ngFor="let group of (groups$ | async)" [value]="group.id">
{{group.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="flex-grow-1"></div>
<mat-form-field *ngIf="canEditRecipesPublicData" class="w-auto">
<mat-label>Note</mat-label>
<textarea
matInput
cols="40" rows="3"
[(ngModel)]="selectedGroupNote"
(keyup)="hasModifications = true">
</textarea>
</mat-form-field>
<p *ngIf="!canEditRecipesPublicData">{{selectedGroupNote}}</p>
</div>
<div class="recipe-content d-flex flex-row justify-content-around align-items-start flex-wrap mt-5">
<!-- Mixes -->
<div *ngIf="recipe.mixes.length > 0">
<cre-mixes-card
[recipe]="recipe"
[deductErrorBody]="deductErrorBody"
[units$]="units$"
(quantityChange)="changeQuantity($event)"
(locationChange)="changeMixLocation($event)"
(deduct)="showDeductMixConfirm($event, deductConfirmBox)">
</cre-mixes-card>
</div>
<!-- Steps -->
<div>
<cre-step-list [recipe]="recipe" [selectedGroupId]="selectedGroupId"></cre-step-list>
</div>
<!-- Images -->
<div *ngIf="recipe.imagesUrls">
<cre-images-editor [recipe]="recipe" [editionMode]="false"></cre-images-editor>
</div>
</div>
</div>
<cre-confirm-box
#deductConfirmBox
message="Voulez-vous vraiment déduire les quantités de ce mélange?"
(click)="deductMix()">
</cre-confirm-box>

View File

@ -1,2 +0,0 @@
.recipe-content > div
margin: 0 3rem 3rem

View File

@ -1,9 +0,0 @@
mat-expansion-panel
width: 60rem
margin: 20px auto
.button-add
margin-top: .8rem
.recipe-color-circle
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)

View File

@ -1,91 +0,0 @@
import {ChangeDetectorRef, Component} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {RecipeService} from '../../services/recipe.service'
import {Permission} from '../../../shared/model/user'
import {AccountService} from '../../../accounts/services/account.service'
import {getRecipeLuma, Recipe} from '../../../shared/model/recipe.model'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service'
import {AppState} from '../../../shared/app-state'
import {ConfigService} from '../../../shared/service/config.service'
import {Config} from '../../../shared/model/config.model'
@Component({
selector: 'cre-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
recipes: { company: string, recipes: Recipe[] }[] = []
tableCols = ['name', 'description', 'color', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
searchQuery = ''
panelForcedExpanded = false
hiddenRecipes = []
constructor(
private recipeService: RecipeService,
private accountService: AccountService,
private configService: ConfigService,
private cdRef: ChangeDetectorRef,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.appState.title = "Explorateur"
this.subscribe(
this.configService.get(Config.EMERGENCY_MODE),
config => {
if (config.content === "false") {
this.subscribe(
this.recipeService.allSortedByCompany,
recipes => this.recipes = recipes
)
} else {
this.urlUtils.navigateTo("/admin/config")
}
}
)
}
searchRecipes() {
if (this.searchQuery.length > 0 && !this.panelForcedExpanded) {
this.panelForcedExpanded = true
this.cdRef.detectChanges()
}
this.recipes
.flatMap(r => r.recipes)
.forEach(r => this.recipeMatchesSearchQuery(r))
}
isCompanyHidden(companyRecipes: Recipe[]): boolean {
return (this.searchQuery && this.searchQuery.length > 0) && companyRecipes.map(r => this.hiddenRecipes[r.id]).filter(r => !r).length <= 0
}
isLight(recipe: Recipe): boolean {
return getRecipeLuma(recipe) > 200
}
get hasEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_RECIPES)
}
private recipeMatchesSearchQuery(recipe: Recipe) {
const matches = this.searchString(recipe.company.name) ||
this.searchString(recipe.name) ||
this.searchString(recipe.description) ||
(recipe.sample && this.searchString(recipe.sample.toString()))
this.hiddenRecipes[recipe.id] = !matches
}
private searchString(value: string): boolean {
return value.toLowerCase().indexOf(this.searchQuery.toLowerCase()) >= 0
}
}

View File

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

View File

@ -1,45 +0,0 @@
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 {ErrorHandlingComponent} from '../../../../shared/components/subscribing.component'
import {MixService} from '../../../services/mix.service'
import {ErrorService} from '../../../../shared/service/error.service'
@Component({
selector: 'cre-mix-add',
templateUrl: './mix-add.component.html',
styleUrls: ['./mix-add.component.sass']
})
export class MixAddComponent extends ErrorHandlingComponent {
recipeId: number | null
materials: Material[] | null
constructor(
private materialService: MaterialService,
private mixService: MixService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit(): void {
super.ngOnInit()
this.recipeId = this.urlUtils.parseIntUrlParam('recipeId')
this.subscribe(
this.materialService.getAllForMixCreation(this.recipeId),
m => this.materials = m
)
}
submit(values) {
this.subscribeAndNavigate(
this.mixService.saveWithUnits(values.name, values.recipeId, values.materialTypeId, values.mixMaterials, values.units),
`/color/edit/${this.recipeId}`
)
}
}

View File

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

View File

@ -1,59 +0,0 @@
import {Component} from '@angular/core'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorHandlingComponent} 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'
import {ErrorHandlerComponent, ErrorService} from '../../../../shared/service/error.service'
import {MixMaterialDto} from '../../../../shared/model/recipe.model'
import {AlertService} from '../../../../shared/service/alert.service'
@Component({
selector: 'cre-mix-edit',
templateUrl: './mix-edit.component.html',
styleUrls: ['./mix-edit.component.sass']
})
export class MixEditComponent extends ErrorHandlingComponent {
mixId: number | null
recipeId: number | null
materials: Material[] | null
constructor(
private materialService: MaterialService,
private mixService: MixService,
private alertService: AlertService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, 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),
m => this.materials = m
)
}
submit(values) {
if(!this.mixMaterialsPositionAreValid(values.mixMaterials)) {
this.alertService.pushError('Les ingrédients ne peuvent pas avoir une position inférieure à 1')
return
}
this.subscribeAndNavigate(
this.mixService.updateWithUnits(this.mixId, values.name, values.materialTypeId, values.mixMaterials, values.units),
`/color/edit/${this.recipeId}`
)
}
private mixMaterialsPositionAreValid(mixMaterials: MixMaterialDto[]): boolean {
return !mixMaterials.find(m => m.position <= 0)
}
}

View File

@ -5,6 +5,9 @@ import { AddComponent } from './pages/add/add.component';
import { EditComponent } from './pages/edit/edit.component'; import { EditComponent } from './pages/edit/edit.component';
import {CompanyRoutingModule} from "./company-routing.module"; import {CompanyRoutingModule} from "./company-routing.module";
import {SharedModule} from "../shared/shared.module"; import {SharedModule} from "../shared/shared.module";
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreTablesModule} from '../shared/components/tables/tables.module'
@ -13,7 +16,10 @@ import {SharedModule} from "../shared/shared.module";
imports: [ imports: [
CommonModule, CommonModule,
CompanyRoutingModule, CompanyRoutingModule,
SharedModule SharedModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
] ]
}) })
export class CompanyModule { } export class CompanyModule { }

View File

@ -1,7 +1,26 @@
<cre-entity-list <cre-action-bar [reverse]="true">
[entities$]="companies$" <cre-action-group>
[columns]="columns" <cre-accent-button routerLink="/catalog/company/add">Ajouter</cre-accent-button>
[buttons]="buttons" </cre-action-group>
addLink="/catalog/company/add" </cre-action-bar>
addPermission="EDIT_COMPANIES">
</cre-entity-list> <cre-warning-alert *ngIf="companiesEmpty">
<p>Il n'y a actuellement aucune bannière enregistrée dans le système.</p>
<p *ngIf="hasEditPermission">Vous pouvez en créer une <b><a routerLink="/catalog/company/add">ici</a></b>.</p>
</cre-warning-alert>
<cre-table *ngIf="!companiesEmpty" class="mx-auto" [data]="companies$ | async" [columns]="columns">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let company">{{company.name}}</td>
</ng-container>
<ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!hasEditPermission" *matCellDef="let company; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/catalog/company/edit/{{company.id}}">
Modifier
</cre-accent-button>
</td>
</ng-container>
</cre-table>

View File

@ -5,6 +5,8 @@ import {Permission} from '../../../shared/model/user'
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service' import {ErrorService} from '../../../shared/service/error.service'
import {AppState} from '../../../shared/app-state' import {AppState} from '../../../shared/app-state'
import {tap} from 'rxjs/operators'
import {AccountService} from '../../../accounts/services/account.service'
@Component({ @Component({
selector: 'cre-list', selector: 'cre-list',
@ -12,18 +14,14 @@ import {AppState} from '../../../shared/app-state'
styleUrls: ['./list.component.sass'] styleUrls: ['./list.component.sass']
}) })
export class ListComponent extends ErrorHandlingComponent { export class ListComponent extends ErrorHandlingComponent {
companies$ = this.companyService.all companies$ = this.companyService.all.pipe(tap(companies => this.companiesEmpty = companies.length <= 0))
columns = [ companiesEmpty = false
{def: 'name', title: 'Nom', valueFn: c => c.name}
] columns = ['name', 'editButton']
buttons = [{
text: 'Modifier',
linkFn: t => `/catalog/company/edit/${t.id}`,
permission: Permission.EDIT_COMPANIES
}]
constructor( constructor(
private companyService: CompanyService, private companyService: CompanyService,
private accountService: AccountService,
private appState: AppState, private appState: AppState,
errorService: ErrorService, errorService: ErrorService,
router: Router, router: Router,
@ -32,4 +30,8 @@ export class ListComponent extends ErrorHandlingComponent {
super(errorService, activatedRoute, router) super(errorService, activatedRoute, router)
this.appState.title = 'Bannières' this.appState.title = 'Bannières'
} }
get hasEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
} }

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ApiService} from "../../shared/service/api.service"; import {ApiService} from '../../shared/service/api.service';
import {Observable} from "rxjs"; import {Observable} from 'rxjs';
import {Company} from "../../shared/model/company.model"; import {Company} from '../../shared/model/company.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -11,23 +11,23 @@
<div class="d-flex flex-column" style="gap: 1.5rem"> <div class="d-flex flex-column" style="gap: 1.5rem">
<cre-config-section *ngIf="!emergencyMode" label="Apparence"> <cre-config-section *ngIf="!emergencyMode" label="Apparence">
<cre-config-list> <cre-config-list>
<!-- <cre-image-config--> <cre-image-config
<!-- label="Logo"--> label="Logo"
<!-- tooltip="Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le--> tooltip="Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
<!-- rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches--> rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
<!-- 'Ctrl+F5')."--> 'Ctrl+F5')."
<!-- [configControl]="getConfigControl(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"--> [configControl]="getConfigControl(keys.INSTANCE_LOGO_SET)" previewWidth="170px"
<!-- (invalidFormat)="invalidFormatConfirmBox.show()">--> (invalidFormat)="invalidFormatConfirmBox.show()">
<!-- </cre-image-config>--> </cre-image-config>
<!-- <cre-image-config--> <cre-image-config
<!-- label="Icône"--> label="Icône"
<!-- tooltip="Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le--> tooltip="Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
<!-- rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches--> rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
<!-- 'Ctrl+F5')."--> 'Ctrl+F5')."
<!-- [configControl]="getConfigControl(keys.INSTANCE_ICON_PATH)" previewWidth="32px"--> [configControl]="getConfigControl(keys.INSTANCE_ICON_SET)" previewWidth="32px"
<!-- (invalidFormat)="invalidFormatConfirmBox.show()">--> (invalidFormat)="invalidFormatConfirmBox.show()">
<!-- </cre-image-config>--> </cre-image-config>
</cre-config-list> </cre-config-list>
</cre-config-section> </cre-config-section>

View File

@ -16,8 +16,8 @@ export class CreConfigEditor extends ErrorHandlingComponent {
keys = { keys = {
INSTANCE_NAME: Config.INSTANCE_NAME, INSTANCE_NAME: Config.INSTANCE_NAME,
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH, INSTANCE_LOGO_SET: Config.INSTANCE_LOGO_SET,
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH, INSTANCE_ICON_SET: Config.INSTANCE_ICON_SET,
INSTANCE_URL: Config.INSTANCE_URL, INSTANCE_URL: Config.INSTANCE_URL,
DATABASE_URL: Config.DATABASE_URL, DATABASE_URL: Config.DATABASE_URL,
DATABASE_USER: Config.DATABASE_USER, DATABASE_USER: Config.DATABASE_USER,

View File

@ -18,7 +18,7 @@
<div class="image-wrapper d-flex flex-column justify-content-end"> <div class="image-wrapper d-flex flex-column justify-content-end">
<div> <div>
<img <img
[src]="updatedImage ? updatedImage : configuredImageUrl" [src]="updatedImage ? updatedImage : imageUrl"
[attr.width]="previewWidth ? previewWidth : null" [attr.width]="previewWidth ? previewWidth : null"
class="mat-elevation-z3"/> class="mat-elevation-z3"/>
</div> </div>

View File

@ -1,10 +1,20 @@
import {AfterViewInit, Component, ContentChild, Directive, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core' import {
AfterViewInit,
Component,
ContentChild,
Directive,
EventEmitter,
Input,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core'
import {ConfigService} from '../shared/service/config.service' import {ConfigService} from '../shared/service/config.service'
import {Config, ConfigControl} from '../shared/model/config.model' import {Config, ConfigControl} from '../shared/model/config.model'
import {SubscribingComponent} from '../shared/components/subscribing.component' import {SubscribingComponent} from '../shared/components/subscribing.component'
import {ErrorService} from '../shared/service/error.service' import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils' import {formatDate, formatDateTime, getConfiguredImageUrl, getFileUrl, readFile} from '../shared/utils/utils'
import {AbstractControl} from '@angular/forms' import {AbstractControl} from '@angular/forms'
import {CrePromptDialog} from '../shared/components/dialogs/dialogs' import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@ -121,9 +131,9 @@ export class CreImageConfig extends _CreConfigBase {
readFile(file, (content) => this.updatedImage = content) readFile(file, (content) => this.updatedImage = content)
} }
get imageUrl(): string {
get configuredImageUrl(): string { const path = this.config.key == Config.INSTANCE_ICON_SET ? 'icon' : 'logo'
return getFileUrl(this.config.content) return getConfiguredImageUrl(path)
} }
} }

View File

@ -6,6 +6,9 @@ import { ListComponent } from './pages/list/list.component';
import {SharedModule} from "../shared/shared.module"; import {SharedModule} from "../shared/shared.module";
import { AddComponent } from './pages/add/add.component'; import { AddComponent } from './pages/add/add.component';
import { EditComponent } from './pages/edit/edit.component'; import { EditComponent } from './pages/edit/edit.component';
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreTablesModule} from '../shared/components/tables/tables.module'
@NgModule({ @NgModule({
@ -13,7 +16,10 @@ import { EditComponent } from './pages/edit/edit.component';
imports: [ imports: [
CommonModule, CommonModule,
MaterialTypeRoutingModule, MaterialTypeRoutingModule,
SharedModule SharedModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
] ]
}) })
export class MaterialTypeModule { } export class MaterialTypeModule { }

View File

@ -1,7 +1,40 @@
<cre-entity-list <cre-action-bar [reverse]="true">
[entities$]="materialTypes$" <cre-action-group>
[columns]="columns" <cre-accent-button routerLink="/catalog/materialtype/add">Ajouter</cre-accent-button>
[buttons]="buttons" </cre-action-group>
addLink="/catalog/materialtype/add" </cre-action-bar>
addPermission="EDIT_MATERIAL_TYPES">
</cre-entity-list> <cre-warning-alert *ngIf="materialTypesEmpty">
<p>Il n'y a actuellement aucun type de produit enregistré dans le système.</p>
<p *ngIf="hasEditPermission">Vous pouvez en créer un <b><a routerLink="/catalog/materialtype/add">ici</a></b>.</p>
</cre-warning-alert>
<cre-table
*ngIf="!materialTypesEmpty"
class="mx-auto"
[data]="materialTypes$ | async"
[columns]="columns">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let materialType">{{materialType.name}}</td>
</ng-container>
<ng-container matColumnDef="prefix">
<th mat-header-cell *matHeaderCellDef>Préfix</th>
<td mat-cell *matCellDef="let materialType">{{materialType.prefix}}</td>
</ng-container>
<ng-container matColumnDef="usePercentages">
<th mat-header-cell *matHeaderCellDef>Utilise les pourcentages</th>
<td mat-cell *matCellDef="let materialType">{{materialType.usePercentages ? 'Oui' : 'Non'}}</td>
</ng-container>
<ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!hasEditPermission" *matCellDef="let materialType; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/catalog/materialtype/edit/{{materialType.id}}">
Modifier
</cre-accent-button>
</td>
</ng-container>
</cre-table>

View File

@ -5,6 +5,8 @@ import {Permission} from '../../../shared/model/user'
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service' import {ErrorService} from '../../../shared/service/error.service'
import {AppState} from '../../../shared/app-state' import {AppState} from '../../../shared/app-state'
import {tap} from 'rxjs/operators'
import {AccountService} from '../../../accounts/services/account.service'
@Component({ @Component({
selector: 'cre-list', selector: 'cre-list',
@ -12,23 +14,16 @@ import {AppState} from '../../../shared/app-state'
styleUrls: ['./list.component.sass'] styleUrls: ['./list.component.sass']
}) })
export class ListComponent extends ErrorHandlingComponent { export class ListComponent extends ErrorHandlingComponent {
materialTypes$ = this.materialTypeService.all materialTypes$ = this.materialTypeService.all.pipe(
columns = [ tap(materialTypes => this.materialTypesEmpty = materialTypes.length <= 0)
{def: 'name', title: 'Nom', valueFn: t => t.name}, )
{def: 'prefix', title: 'Préfixe', valueFn: t => t.prefix}, materialTypesEmpty = false
{def: 'usePercentages', title: 'Utilise les pourcentages', valueFn: t => t.usePercentages ? 'Oui' : 'Non'}
] columns = ['name', 'prefix', 'usePercentages', 'editButton']
buttons = [
{
text: 'Modifier',
linkFn: t => `/catalog/materialtype/edit/${t.id}`,
permission: Permission.EDIT_MATERIAL_TYPES,
disabledFn: t => t.systemType
}
]
constructor( constructor(
private materialTypeService: MaterialTypeService, private materialTypeService: MaterialTypeService,
private accountService: AccountService,
private appState: AppState, private appState: AppState,
errorService: ErrorService, errorService: ErrorService,
router: Router, router: Router,
@ -37,4 +32,8 @@ export class ListComponent extends ErrorHandlingComponent {
super(errorService, activatedRoute, router) super(errorService, activatedRoute, router)
this.appState.title = 'Types de produit' this.appState.title = 'Types de produit'
} }
get hasEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
} }

View File

@ -6,9 +6,13 @@ import {InventoryComponent} from './pages/inventory/inventory.component';
import {SharedModule} from "../shared/shared.module"; import {SharedModule} from "../shared/shared.module";
import {AddComponent} from './pages/add/add.component'; import {AddComponent} from './pages/add/add.component';
import {EditComponent} from './pages/edit/edit.component'; import {EditComponent} from './pages/edit/edit.component';
import {ColorsModule} from '../colors/colors.module' import {RecipesModule} from '../recipes/recipes.module'
import {MatSortModule} from '@angular/material/sort' import {MatSortModule} from '@angular/material/sort'
import {FormsModule} from '@angular/forms' import {FormsModule} from '@angular/forms'
import {CreTablesModule} from '../shared/components/tables/tables.module'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
@NgModule({ @NgModule({
@ -17,9 +21,13 @@ import {FormsModule} from '@angular/forms'
CommonModule, CommonModule,
MaterialRoutingModule, MaterialRoutingModule,
SharedModule, SharedModule,
ColorsModule, RecipesModule,
MatSortModule, MatSortModule,
FormsModule FormsModule,
CreTablesModule,
CreInputsModule,
CreButtonsModule,
CreActionBarModule
] ]
}) })
export class MaterialModule { export class MaterialModule {

View File

@ -1,62 +1,59 @@
<div class="action-bar backward"> <cre-action-bar>
<!-- Left --> <cre-action-group>
<div class="d-flex flex-row"> <cre-input
<mat-form-field class="mr-4"> class="mr-4"
<mat-label>Recherche par code...</mat-label> label="Recherche par code..."
<input [control]="materialNameFilterControl">
matInput </cre-input>
type="text" <cre-select
[(ngModel)]="materialNameFilter" class="mr-4"
(keyup)="filterDataSource()"/> label="Recherche par type de produit"
</mat-form-field> [control]="materialTypeFilterControl"
<mat-form-field *ngIf="materialTypes$ | async as materialTypes"> [entries]="materialTypesEntries$">
<mat-label>Recherche par type de produit</mat-label> </cre-select>
<mat-select <cre-checkbox-input
[(value)]="materialTypeFilter" label="Basse quantité"
(valueChange)="filterDataSource()"> [control]="hideLowQuantityControl">
<mat-option </cre-checkbox-input>
*ngFor="let materialType of materialTypes" </cre-action-group>
[value]="materialType.id"> <cre-action-group>
{{materialType.name}} <cre-input
</mat-option> class="mr-4"
</mat-select> label="Quantité faible"
</mat-form-field> type="number"
</div> step="0.01"
[(value)]="lowQuantityThreshold">
<!-- Right --> </cre-input>
<div class="ml-auto">
<mat-form-field class="mr-4">
<mat-label>Quantité faible</mat-label>
<input
matInput
type="number"
step="0.01"
[(ngModel)]="lowQuantityThreshold"/>
</mat-form-field>
<cre-unit-selector [(unit)]="units"></cre-unit-selector> <cre-unit-selector [(unit)]="units"></cre-unit-selector>
<button <cre-accent-button
*ngIf="canEditMaterial" *ngIf="canEditMaterial"
class="ml-3" class="ml-3"
mat-raised-button
color="accent"
routerLink="/catalog/material/add"> routerLink="/catalog/material/add">
Ajouter Ajouter
</button> </cre-accent-button>
</div> </cre-action-group>
</div> </cre-action-bar>
<table <cre-warning-alert *ngIf="!loading && materials.length === 0">
mat-table <p>Il n'y a actuellement aucun produit enregistré dans le système.</p>
matSort <p *ngIf="canEditMaterial">Vous pouvez en créer un <b><a routerLink="/catalog/material/add">ici</a></b>.
</p>
</cre-warning-alert>
<cre-table
*ngIf="materials.length > 0"
class="mx-auto" class="mx-auto"
[dataSource]="dataSource"> [filterPredicate]="materialFilterPredicate"
[filter]="filter"
[data]="materials"
[columns]="columns">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Code</th> <th mat-header-cell *matHeaderCellDef>Code</th>
<td mat-cell *matCellDef="let material">{{material.name}}</td> <td mat-cell *matCellDef="let material">{{material.name}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="materialType"> <ng-container matColumnDef="materialType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type de produit</th> <th mat-header-cell *matHeaderCellDef>Type de produit</th>
<td mat-cell *matCellDef="let material">{{material.materialType.name}}</td> <td mat-cell *matCellDef="let material">{{material.materialType.name}}</td>
</ng-container> </ng-container>
@ -68,9 +65,7 @@
<ng-container matColumnDef="addQuantity"> <ng-container matColumnDef="addQuantity">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canAddToInventory" *matCellDef="let material; let i = index"> <td mat-cell [class.disabled]="!canAddToInventory" *matCellDef="let material; let i = index">
<div <div [creInteractiveCell]="i" class="input-group">
[hidden]="!((!hoveredMaterial && i === 0) || (hoveredMaterial === material) || (selectedMaterial && selectedMaterial === material))"
class="input-group">
<input <input
#addQuantityInput #addQuantityInput
class="form-control w-50" class="form-control w-50"
@ -91,7 +86,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="lowQuantityIcon"> <ng-container matColumnDef="lowQuantityIcon">
<th mat-header-cell *matHeaderCellDef mat-sort-header></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)"> <td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)">
<mat-icon <mat-icon
svgIcon="format-color-fill" svgIcon="format-color-fill"
@ -115,37 +110,23 @@
<ng-container matColumnDef="editButton"> <ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial"> <td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial">
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material"> <cre-accent-button
<button [creInteractiveCell]="i"
mat-raised-button routerLink="/catalog/material/edit/{{material.id}}">
color="accent" Modifier
routerLink="/catalog/material/edit/{{material.id}}"> </cre-accent-button>
Modifier
</button>
</ng-container>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="openSimdutButton"> <ng-container matColumnDef="openSimdutButton">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="canEditMaterial"> <td mat-cell *matCellDef="let material; let i = index" [class.disabled]="canEditMaterial">
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material"> <cre-accent-button
<button [creInteractiveCell]="i"
mat-raised-button [disabled]="!materialHasSimdut(material)"
color="accent" (click)="openSimdut(material)">
[disabled]="!materialHasSimdut(material)" Fiche signalitique
(click)="openSimdut(material)"> </cre-accent-button>
Fiche signalitique
</button>
</ng-container>
</td> </td>
</ng-container> </ng-container>
</cre-table>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr
mat-row
class="entity-row"
*matRowDef="let material; columns: columns"
(mouseover)="hoveredMaterial = material">
</tr>
</table>

View File

@ -1,8 +1,9 @@
.input-group-append button
border-radius: 0 4px 4px 0
mat-select mat-select
margin-top: 4px margin-top: 4px
.form-control .input-group
width: 6rem cre-input
width: 6rem
.input-group-append button
border-radius: 0 4px 4px 0

View File

@ -4,14 +4,17 @@ import {MaterialService} from '../../service/material.service'
import {Permission} from '../../../shared/model/user' import {Permission} from '../../../shared/model/user'
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service' import {ErrorService} from '../../../shared/service/error.service'
import {Material, openSimdut} from '../../../shared/model/material.model' import {Material, materialFilterFieldSeparator, materialMatchesFilter, openSimdut} from '../../../shared/model/material.model'
import {AccountService} from '../../../accounts/services/account.service' import {AccountService} from '../../../accounts/services/account.service'
import {convertQuantity, UNIT_MILLILITER} from '../../../shared/units' import {convertQuantity, UNIT_MILLILITER} from '../../../shared/units'
import {MatSort} from '@angular/material/sort' import {MatSort} from '@angular/material/sort'
import {MatTableDataSource} from '@angular/material/table'
import {MaterialTypeService} from '../../../material-type/service/material-type.service' import {MaterialTypeService} from '../../../material-type/service/material-type.service'
import {InventoryService} from '../../service/inventory.service' import {InventoryService} from '../../service/inventory.service'
import {AppState} from '../../../shared/app-state' import {AppState} from '../../../shared/app-state'
import {FormControl} from '@angular/forms'
import {map} from 'rxjs/operators'
import {CreInputEntry} from '../../../shared/components/inputs/inputs'
import {round} from '../../../shared/utils/utils'
@Component({ @Component({
selector: 'cre-list', selector: 'cre-list',
@ -21,9 +24,10 @@ import {AppState} from '../../../shared/app-state'
export class InventoryComponent extends ErrorHandlingComponent { export class InventoryComponent extends ErrorHandlingComponent {
@ViewChild(MatSort) sort: MatSort @ViewChild(MatSort) sort: MatSort
materials: Material[] | null materials: Material[] | null = []
materialTypes$ = this.materialTypeService.all materialTypesEntries$ = this.materialTypeService.all.pipe(map(materialTypes => {
dataSource: MatTableDataSource<Material> return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))
}))
columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton'] columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton']
hoveredMaterial: Material | null hoveredMaterial: Material | null
@ -31,8 +35,16 @@ export class InventoryComponent extends ErrorHandlingComponent {
units = UNIT_MILLILITER units = UNIT_MILLILITER
lowQuantityThreshold = 100 // TEMPORARY will be in the application settings lowQuantityThreshold = 100 // TEMPORARY will be in the application settings
materialTypeFilter = 1
materialNameFilter = '' materialFilterPredicate = materialMatchesFilter
private materialTypeFilter = 1
private materialNameFilter = ''
private hideLowQuantity = false
materialTypeFilterControl = new FormControl(this.materialTypeFilter)
materialNameFilterControl = new FormControl(this.materialNameFilter)
hideLowQuantityControl = new FormControl(this.hideLowQuantity)
constructor( constructor(
private materialService: MaterialService, private materialService: MaterialService,
@ -53,38 +65,25 @@ export class InventoryComponent extends ErrorHandlingComponent {
this.subscribe( this.subscribe(
this.materialService.allNotMixType, this.materialService.allNotMixType,
materials => { materials => this.materials = materials,
this.materials = materials
this.dataSource = this.setupDataSource()
},
true, true,
1 1
) )
}
setupDataSource(): MatTableDataSource<Material> { this.subscribe(
this.dataSource = new MatTableDataSource<Material>(this.materials) this.materialTypeFilterControl.valueChanges,
this.dataSource.sortingDataAccessor = (material, header) => { filter => this.materialTypeFilter = filter
switch (header) { )
case 'materialType':
return material[header].name
case 'lowQuantityIcon':
return this.isLowQuantity(material)
default:
return material[header]
}
}
this.dataSource.filterPredicate = (material, filter) => {
return (!this.materialTypeFilter || this.materialTypeFilter === 1 || material.materialType.id === this.materialTypeFilter) &&
(!this.materialNameFilter || material.name.toLowerCase().includes(this.materialNameFilter.toLowerCase()))
}
this.dataSource.sort = this.sort this.subscribe(
return this.dataSource this.materialNameFilterControl.valueChanges,
} filter => this.materialNameFilter = filter
)
filterDataSource() { this.subscribe(
this.dataSource.filter = 'filter' this.hideLowQuantityControl.valueChanges,
filter => this.hideLowQuantity = filter
)
} }
isLowQuantity(material: Material) { isLowQuantity(material: Material) {
@ -92,7 +91,7 @@ export class InventoryComponent extends ErrorHandlingComponent {
} }
getQuantity(material: Material): number { getQuantity(material: Material): number {
return Math.round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units) * 100) / 100 return round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units), 2)
} }
materialHasSimdut(material: Material): boolean { materialHasSimdut(material: Material): boolean {
@ -118,6 +117,10 @@ export class InventoryComponent extends ErrorHandlingComponent {
) )
} }
get filter(): string {
return [this.materialTypeFilter, this.materialNameFilter, this.hideLowQuantity, this.lowQuantityThreshold].join(materialFilterFieldSeparator)
}
get canEditMaterial(): boolean { get canEditMaterial(): boolean {
return this.accountService.hasPermission(Permission.EDIT_MATERIALS) return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
} }

View File

@ -0,0 +1,10 @@
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-submit-button [form]="recipeForm.creForm" (submit)="recipeForm.submit()"></cre-submit-button>
</cre-action-group>
</cre-action-bar>
<recipe-form #recipeForm (submitForm)="submit($event)"></recipe-form>

View File

@ -4,6 +4,8 @@
</mat-card-header> </mat-card-header>
<mat-card-content [class.no-action]="!editionMode"> <mat-card-content [class.no-action]="!editionMode">
<div class="d-flex flex-row justify-content-around flex-wrap"> <div class="d-flex flex-row justify-content-around flex-wrap">
<p *ngIf="imagesUrls.length <= 0" class="light-text text-center mb-0">Aucune image n'est associée à cette couleur</p>
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3"> <div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
<div class="image-wrapper"> <div class="image-wrapper">
<img [src]="imageUrl" width="300px"/> <img [src]="imageUrl" width="300px"/>

View File

@ -1,5 +1,5 @@
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core' import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsAsMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model' import {Mix, MixMaterial, MixMaterialDto, mixMaterialsToMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
import {Subject} from 'rxjs' import {Subject} from 'rxjs'
import {SubscribingComponent} from '../../../shared/components/subscribing.component' import {SubscribingComponent} from '../../../shared/components/subscribing.component'
import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units' import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units'
@ -60,7 +60,7 @@ export class MixTableComponent extends SubscribingComponent {
this.mixColumns = this.COLUMNS_EDIT this.mixColumns = this.COLUMNS_EDIT
} }
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix) this.mixMaterials = mixMaterialsToMixMaterialsDto(this.mix)
this.subscribe( this.subscribe(
this.units$, this.units$,
@ -191,12 +191,13 @@ export class MixTableComponent extends SubscribingComponent {
materialId: quantity.materialId, materialId: quantity.materialId,
quantity: this.calculateQuantity(index), quantity: this.calculateQuantity(index),
isPercents: quantity.isPercents, isPercents: quantity.isPercents,
position: quantity.position position: quantity.position,
units: UNIT_MILLILITER
}) })
} }
private convertQuantities(newUnit: string) { private convertQuantities(newUnit: string) {
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, this.units, newUnit)) this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, newUnit))
this.units = newUnit this.units = newUnit
} }

View File

@ -3,6 +3,8 @@
<mat-card-title>Mélanges</mat-card-title> <mat-card-title>Mélanges</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content [class.no-action]="!editionMode"> <mat-card-content [class.no-action]="!editionMode">
<p *ngIf="recipe.mixes.length <= 0" class="light-text text-center">Il n'y a aucun mélange dans cette couleur</p>
<ng-container *ngFor="let mix of recipe.mixes; let i = index"> <ng-container *ngFor="let mix of recipe.mixes; let i = index">
<cre-mix-table <cre-mix-table
[class.no-top-margin]="i == 0" [class.no-top-margin]="i == 0"

View File

@ -3,7 +3,7 @@
<mat-card-title>Étapes</mat-card-title> <mat-card-title>Étapes</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content class="no-action"> <mat-card-content class="no-action">
<mat-list> <mat-list *ngIf="steps.length > 0">
<mat-list-item *ngFor="let step of steps"> <mat-list-item *ngFor="let step of steps">
{{step.position}}.<span class="space"></span>{{step.message}} {{step.position}}.<span class="space"></span>{{step.message}}
</mat-list-item> </mat-list-item>

View File

@ -1,6 +1,6 @@
<mat-form-field [class.short]="short"> <mat-form-field [class.short]="short">
<mat-label *ngIf="showLabel">Unités</mat-label> <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"> <ng-container *ngIf="!short">
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option> <mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
<mat-option [value]="unitConstants.UNIT_LITER">Litres</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 {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
import {FormControl} from '@angular/forms'
@Component({ @Component({
selector: 'cre-unit-selector', selector: 'cre-unit-selector',
templateUrl: './unit-selector.component.html', templateUrl: './unit-selector.component.html',
styleUrls: ['./unit-selector.component.sass'] styleUrls: ['./unit-selector.component.sass']
}) })
export class UnitSelectorComponent { export class UnitSelectorComponent implements OnInit {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON} readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
@Input() unit = UNIT_MILLILITER @Input() unit = UNIT_MILLILITER
@Input() showLabel = true @Input() showLabel = true
@Input() short = false @Input() short = false
@Input() control: FormControl | null
@Output() unitChange = new EventEmitter<string>() @Output() unitChange = new EventEmitter<string>()
ngOnInit() {
this.control?.setValue(this.unit)
}
onUnitChange(newUnit: string) {
this.control?.setValue(newUnit)
this.unitChange.emit(newUnit)
}
} }

View File

@ -0,0 +1,36 @@
<div *ngIf="recipe">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
</cre-action-group>
<cre-action-group>
<cre-warn-button (click)="deleteConfirmBox.show()">Supprimer</cre-warn-button>
<cre-accent-button (click)="submit()">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div class="recipe-wrapper d-flex flex-row justify-content-around align-items-start flex-wrap">
<section>
<recipe-form [recipe]="recipe"></recipe-form>
</section>
<section>
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
</section>
<section>
<cre-step-table [recipe]="recipe" [groups$]="groups$" [selectedGroupId]="loggedInUserGroupId"></cre-step-table>
</section>
<section>
<cre-images-editor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
</section>
</div>
</div>
<cre-confirm-box
#deleteConfirmBox
message="Voulez-vous vraiment supprimer la couleur {{recipe?.name}}?"
(confirm)="delete()">
</cre-confirm-box>

View File

@ -0,0 +1,52 @@
<div *ngIf="recipe">
<cre-recipe-info [recipe]="recipe" [hasModifications]="hasModifications"></cre-recipe-info>
<cre-action-bar>
<cre-action-group>
<cre-action-group>
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
<cre-select [control]="groupControl" label="Group" [entries]="groupEntries$"></cre-select>
</cre-action-group>
<cre-action-group>
<cre-textarea [control]="noteControl" [cols]="50" [rows]="canEditRecipesPublicData ? 2 : 1"></cre-textarea>
</cre-action-group>
</cre-action-group>
<cre-action-group>
<cre-primary-button disabled title="WIP">Version Excel</cre-primary-button>
<cre-accent-button *ngIf="canEditRecipesPublicData" [disabled]="!hasModifications" (click)="saveModifications()">
Enregistrer
</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div class="recipe-content d-flex flex-row justify-content-around align-items-start flex-wrap">
<!-- Mixes -->
<div *ngIf="recipe.mixes.length > 0">
<cre-mixes-card
[recipe]="recipe"
[deductErrorBody]="deductErrorBody"
[units$]="units$"
(quantityChange)="changeQuantity($event)"
(locationChange)="changeMixLocation($event)"
(deduct)="showDeductMixConfirm($event, deductConfirmBox)">
</cre-mixes-card>
</div>
<!-- Steps -->
<div>
<cre-step-list [recipe]="recipe" [selectedGroupId]="selectedGroupId"></cre-step-list>
</div>
<!-- Images -->
<div *ngIf="recipe.imagesUrls">
<cre-images-editor [recipe]="recipe" [editionMode]="false"></cre-images-editor>
</div>
</div>
</div>
<cre-confirm-box
#deductConfirmBox
message="Voulez-vous vraiment déduire les quantités de ce mélange?"
(click)="deductMix()">
</cre-confirm-box>

View File

@ -1,27 +1,34 @@
import {Component} from '@angular/core' import {Component} from '@angular/core'
import {RecipeService} from '../../services/recipe.service' import {RecipeService} from './services/recipe.service'
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component' import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
import {MixMaterialDto, Recipe, recipeMixCount, recipeNoteForGroupId, recipeStepCount} from '../../../shared/model/recipe.model' import {
MixMaterialDto,
Recipe,
recipeMixCount,
recipeNoteForGroupId,
recipeStepCount
} from '../shared/model/recipe.model'
import {Observable, Subject} from 'rxjs' import {Observable, Subject} from 'rxjs'
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service' import {ErrorHandler, ErrorService} from '../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service' import {AlertService} from '../shared/service/alert.service'
import {GlobalAlertHandlerComponent} from '../../../shared/components/global-alert-handler/global-alert-handler.component' import {GlobalAlertHandlerComponent} from '../shared/components/global-alert-handler/global-alert-handler.component'
import {InventoryService} from '../../../material/service/inventory.service' import {InventoryService} from '../material/service/inventory.service'
import {ConfirmBoxComponent} from '../../../shared/components/confirm-box/confirm-box.component' import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
import {GroupService} from '../../../groups/services/group.service' import {GroupService} from '../groups/services/group.service'
import {AppState} from '../../../shared/app-state' import {AppState} from '../shared/app-state'
import {AccountService} from '../../../accounts/services/account.service' import {AccountService} from '../accounts/services/account.service'
import {Permission} from '../../../shared/model/user' import {Permission} from '../shared/model/user'
import {FormControl} from '@angular/forms';
import {map} from 'rxjs/operators';
import {CreInputEntry} from '../shared/components/inputs/inputs';
@Component({ @Component({
selector: 'cre-explore', selector: 'cre-recipe-explore',
templateUrl: './explore.component.html', templateUrl: './explore.html',
styleUrls: ['./explore.component.sass'] styleUrls: ['./recipes.sass']
}) })
export class ExploreComponent extends ErrorHandlingComponent { export class CreRecipeExplore extends ErrorHandlingComponent {
recipe: Recipe | null
groups$ = this.groupService.all
deductErrorBody = {} deductErrorBody = {}
units$ = new Subject<string>() units$ = new Subject<string>()
selectedGroupId: number | null selectedGroupId: number | null
@ -33,6 +40,12 @@ export class ExploreComponent extends ErrorHandlingComponent {
deductedMixId: number | null deductedMixId: number | null
groupControl: FormControl
noteControl: FormControl
groupEntries$ = this.groupService.all.pipe(map(groups => {
return groups.map(group => new CreInputEntry(group.id, group.name))
}))
errorHandlers: ErrorHandler[] = [{ errorHandlers: ErrorHandler[] = [{
filter: error => error.type === 'notfound-recipe-id', filter: error => error.type === 'notfound-recipe-id',
consumer: error => this.urlUtils.navigateTo('/color/list') consumer: error => this.urlUtils.navigateTo('/color/list')
@ -42,6 +55,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
messageProducer: () => 'Certains produit ne sont pas en quantité suffisante dans l\'inventaire' messageProducer: () => 'Certains produit ne sont pas en quantité suffisante dans l\'inventaire'
}] }]
private _recipe: Recipe | null
private _notePlaceholder = !this.canEditRecipesPublicData ? 'N/A' : ''
constructor( constructor(
private recipeService: RecipeService, private recipeService: RecipeService,
private inventoryService: InventoryService, private inventoryService: InventoryService,
@ -62,18 +78,30 @@ export class ExploreComponent extends ErrorHandlingComponent {
this.selectedGroupId = this.loggedInUserGroupId this.selectedGroupId = this.loggedInUserGroupId
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id')) this.fetchRecipe()
this.groupControl = new FormControl(this.selectedGroupId)
this.subscribe(
this.groupControl.valueChanges,
groupId => {
this.selectedGroupId = groupId
this.noteControl.setValue(this.selectedGroupNote, {emitEvent: false})
}
)
this.noteControl = new FormControl({value: this._notePlaceholder, disabled: !this.canEditRecipesPublicData})
this.subscribe(
this.noteControl.valueChanges,
_ => this.hasModifications = true
)
}
fetchRecipe() {
const recipeId = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
this.subscribeEntityById( this.subscribeEntityById(
this.recipeService, this.recipeService,
id, recipeId,
r => { recipe => this.recipe = recipe
this.recipe = r
this.appState.title = r.name
if (recipeMixCount(this.recipe) <= 0 || recipeStepCount(this.recipe) <= 0) {
this.alertService.pushWarning('Cette recette n\'est pas complète')
}
}
) )
} }
@ -128,11 +156,24 @@ export class ExploreComponent extends ErrorHandlingComponent {
subscribeDeductMix(observable: Observable<any>) { subscribeDeductMix(observable: Observable<any>) {
this.subscribe( this.subscribe(
observable, observable,
() => this.alertService.pushSuccess('Les quantités quantités ont été déduites de l\'inventaire'), () => this.alertService.pushSuccess('Les quantités ont été déduites de l\'inventaire'),
true true
) )
} }
get recipe(): Recipe {
return this._recipe
}
set recipe(recipe: Recipe) {
this._recipe = recipe
this.appState.title = recipe.name
if (recipeMixCount(recipe) <= 0 || recipeStepCount(recipe) <= 0) {
this.alertService.pushWarning('Cette recette n\'est pas complète')
}
}
get loggedInUserGroupId(): number { get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id return this.appState.authenticatedUser.group?.id
} }
@ -141,11 +182,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
if (!this.groupsNote.has(this.selectedGroupId)) { if (!this.groupsNote.has(this.selectedGroupId)) {
this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId)) this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId))
} }
return this.groupsNote.get(this.selectedGroupId) return this.groupsNote.get(this.selectedGroupId) ?? this._notePlaceholder
}
set selectedGroupNote(value: string) {
this.groupsNote.set(this.selectedGroupId, value)
} }
get canEditRecipesPublicData(): boolean { get canEditRecipesPublicData(): boolean {
@ -160,7 +197,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
}) })
this.groupsNote.forEach((content, groupId) => { this.groupsNote.forEach((content, groupId) => {
updatedNotes.set(groupId, content) if (content) {
updatedNotes.set(groupId, content)
}
}) })
return updatedNotes return updatedNotes

View File

@ -0,0 +1,21 @@
<div *ngIf="!loading && !hasCompanies" class="mt-5">
<cre-warning-alert>
<p>Il n'y a actuellement aucune bannière enregistrée dans le système.</p>
<p *ngIf="hasCompanyEditPermission">Vous pouvez en créer une <b><a routerLink="/catalog/company/add">ici</a></b>.</p>
</cre-warning-alert>
</div>
<cre-form *ngIf="hasCompanies" [formControls]="controls" class="mx-auto">
<cre-form-title *ngIf="!recipe">Ajouter une couleur</cre-form-title>
<cre-form-title *ngIf="recipe">Modifier la couleur {{recipe.name}}</cre-form-title>
<cre-form-content>
<cre-input [control]="controls.name" label="Name" icon="form-textbox"></cre-input>
<cre-input [control]="controls.description" label="Description" icon="text"></cre-input>
<cre-input [control]="controls.color" type="color" label="Couleur" icon="palette"></cre-input>
<cre-slider-input [control]="controls.gloss" label="Lustre"></cre-slider-input>
<cre-input [control]="controls.sample" type="number" label="Échantillon" icon="pound"></cre-input>
<cre-input [control]="controls.approbationDate" type="date" label="Date d'approbation" icon="calendar"></cre-input>
<cre-input [control]="controls.remark" label="Remarque" icon="text"></cre-input>
<cre-combo-box [control]="controls.company" label="Bannière" [entries]="companyEntries$"></cre-combo-box>
</cre-form-content>
</cre-form>

View File

@ -1,43 +1,47 @@
<div class="action-bar"> <cre-action-bar>
<mat-form-field> <cre-action-group>
<mat-label>Recherche</mat-label> <cre-input label="Recherche" [control]="searchControl"></cre-input>
<input </cre-action-group>
matInput <cre-action-group>
type="text" <cre-accent-button *ngIf="hasEditPermission" routerLink="/color/add">Ajouter</cre-accent-button>
[(ngModel)]="searchQuery" </cre-action-group>
(keyup)="searchRecipes()"/> </cre-action-bar>
<button
mat-button <div *ngIf="!loading">
*ngIf="searchQuery" <cre-warning-alert *ngIf="companies.length === 0">
matSuffix <p>Il n'y a actuellement aucune bannière enregistrée dans le système.</p>
mat-icon-button <p *ngIf="hasCompanyEditPermission">Vous pouvez en créer une <b><a routerLink="/catalog/company/add">ici</a></b>.
(click)="searchQuery=''"> </p>
<mat-icon>close</mat-icon> </cre-warning-alert>
</button> <cre-warning-alert *ngIf="companies.length > 0 && recipes.size === 0">
</mat-form-field> <p>Il n'y a actuellement aucune recette enregistrée dans le système.</p>
<div class="button-add"> <p *ngIf="hasEditPermission">Vous pouvez en créer une <b><a routerLink="/color/add">ici</a></b>.</p>
<button *ngIf="hasEditPermission" mat-raised-button color="accent" routerLink="/color/add">Ajouter</button> </cre-warning-alert>
</div>
</div> </div>
<mat-expansion-panel <mat-expansion-panel
class="table-title" class="table-title"
*ngFor="let companyRecipes of recipes" *ngFor="let company of companies"
[hidden]="isCompanyHidden(companyRecipes.recipes)" [hidden]="isCompanyHidden(company)"
[expanded]="panelForcedExpanded"> [expanded]="panelForcedExpanded">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{companyRecipes.company}} {{company.name}}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: companyRecipes.recipes}"></ng-container> <ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: recipes.get(company.id)}"></ng-container>
</mat-expansion-panel> </mat-expansion-panel>
<ng-template <ng-template
#recipeTableTemplate #recipeTableTemplate
let-recipes="recipes"> let-recipes="recipes">
<table class="mx-auto" mat-table [dataSource]="recipes"> <cre-table
class="w-100"
[columns]="columns"
[data]="recipes"
[filter]="searchQuery"
[filterPredicate]="recipeFilterPredicate">
<!-- Recipe's info --> <!-- Recipe's info -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th> <th mat-header-cell *matHeaderCellDef>Nom</th>
@ -85,19 +89,16 @@
<!-- Buttons --> <!-- Buttons -->
<ng-container matColumnDef="buttonView"> <ng-container matColumnDef="buttonView">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe"> <td mat-cell *matCellDef="let recipe; let i = index">
<button mat-flat-button color="accent" routerLink="/color/explore/{{recipe.id}}">Voir</button> <cre-accent-button [creInteractiveCell]="i" routerLink="/color/explore/{{recipe.id}}">Voir</cre-accent-button>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="buttonEdit"> <ng-container matColumnDef="buttonEdit">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe" [class.disabled]="!hasEditPermission"> <td mat-cell *matCellDef="let recipe; let i = index" [class.disabled]="!hasEditPermission">
<button mat-flat-button color="accent" routerLink="/color/edit/{{recipe.id}}">Modifier</button> <cre-accent-button [creInteractiveCell]="i" routerLink="/color/edit/{{recipe.id}}">Modifier</cre-accent-button>
</td> </td>
</ng-container> </ng-container>
</cre-table>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let recipe; columns: tableCols" [hidden]="hiddenRecipes[recipe.id]"></tr>
</table>
</ng-template> </ng-template>

View File

@ -0,0 +1,110 @@
import {ChangeDetectorRef, Component} from '@angular/core'
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
import {Company} from '../shared/model/company.model'
import {getRecipeLuma, Recipe, recipeMatchesFilter} from '../shared/model/recipe.model'
import {CompanyService} from '../company/service/company.service'
import {RecipeService} from './services/recipe.service'
import {AccountService} from '../accounts/services/account.service'
import {ConfigService} from '../shared/service/config.service'
import {AppState} from '../shared/app-state'
import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {Config} from '../shared/model/config.model'
import {Permission} from '../shared/model/user'
import {FormControl} from '@angular/forms'
@Component({
selector: 'cre-recipe-list',
templateUrl: 'list.html',
styleUrls: ['recipes.sass']
})
export class RecipeList extends ErrorHandlingComponent {
companies: Company[] = []
recipes: Map<number, Recipe[]> = new Map<number, Recipe[]>()
columns = ['name', 'description', 'color', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
panelForcedExpanded = false
searchControl: FormControl
searchQuery = ''
recipeFilterPredicate = recipeMatchesFilter
constructor(
private companyService: CompanyService,
private recipeService: RecipeService,
private accountService: AccountService,
private configService: ConfigService,
private cdRef: ChangeDetectorRef,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.appState.title = 'Explorateur'
// Navigate to configs if server is in emergency mode
this.subscribe(
this.configService.get(Config.EMERGENCY_MODE),
config => {
if (config.content == 'true') {
this.urlUtils.navigateTo('/admin/config/')
}
}
)
this.fetchCompanies()
this.fetchRecipes()
this.searchControl = new FormControl('')
this.subscribe(
this.searchControl.valueChanges,
value => {
this.searchQuery = value
if (value.length > 0 && !this.panelForcedExpanded) {
this.panelForcedExpanded = true
this.cdRef.detectChanges()
}
}
)
}
private fetchCompanies() {
this.subscribe(
this.companyService.all,
companies => this.companies = companies
)
}
private fetchRecipes() {
this.subscribe(
this.recipeService.allByCompany,
recipes => this.recipes = recipes,
true
)
}
isCompanyHidden(company: Company): boolean {
const companyRecipes = this.recipes.get(company.id)
return !(companyRecipes && companyRecipes.length >= 0) ||
this.searchQuery && this.searchQuery.length > 0 &&
!companyRecipes.some(recipe => this.recipeFilterPredicate(recipe, this.searchQuery))
}
isLight(recipe: Recipe): boolean {
return getRecipeLuma(recipe) > 200
}
get hasEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_RECIPES)
}
get hasCompanyEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
}

View File

@ -0,0 +1,20 @@
<ng-container *ngIf="recipe">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/edit/{{recipe.id}}">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-accent-button [disabled]="!form.valid" (click)="submit(form.formValues)">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<cre-mix-form
#form
[recipe]="recipe"
[materialTypes]="materialTypes$"
[materials]="materials$">
<cre-form-title>
Ajouter un mélange à la couleur {{recipe.company.name}} - {{recipe.name}}
</cre-form-title>
</cre-mix-form>
</ng-container>

View File

@ -0,0 +1,25 @@
<ng-container *ngIf="recipe && mix">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/edit/{{recipe.id}}">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-accent-button
[disabled]="!form.valid"
(click)="submit(form.formValues)">
Enregistrer
</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<cre-mix-form
#form
[recipe]="recipe"
[mix]="mix"
[materialTypes]="materialTypes$"
[materials]="materials$">
<cre-form-title>
Modification du mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}}
</cre-form-title>
</cre-mix-form>
</ng-container>

View File

@ -0,0 +1,13 @@
<cre-mix-info-form
[recipe]="recipe"
[mix]="mix"
[materialTypes]="materialTypes">
<cre-form-title>
<ng-content select="cre-form-title"></ng-content>
</cre-form-title>
</cre-mix-info-form>
<cre-mix-materials-form
[materials]="materials"
[mix]="mix">
</cre-mix-materials-form>

View File

@ -0,0 +1,13 @@
<cre-form [formControls]="controls" class="mx-auto">
<!-- <cre-form-title>-->
<!-- Ajouter un mélange à la couleur {{recipe.company.name}} - {{recipe.name}}-->
<!-- </cre-form-title>-->
<cre-form-title>
<ng-content select="cre-form-title"></ng-content>
</cre-form-title>
<cre-form-content>
<cre-input [control]="controls.name" label="Name" icon="form-textbox"></cre-input>
<cre-select [control]="controls.materialType" label="Type de produit" [entries]="materialTypeEntries"></cre-select>
</cre-form-content>
</cre-form>

View File

@ -0,0 +1,5 @@
<cre-combo-box
*ngIf="entries"
[control]="control"
[entries]="entries">
</cre-combo-box>

View File

@ -0,0 +1,76 @@
<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" [data]="mixMaterials" [columns]="columns">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.position}}</td>
</ng-container>
<ng-container matColumnDef="positionButtons">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-table-position-buttons
[position]="mixMaterial.position"
[min]="1"
[max]="mixMaterials.length"
[disableDecreaseButton]="isDecreasePositionButtonDisabled(mixMaterial)"
[disableIncreaseButton]="isIncreasePositionButtonDisabled(mixMaterial)"
(positionChange)="updatePosition(mixMaterial, $event)">
</cre-table-position-buttons>
</td>
</ng-container>
<ng-container matColumnDef="material">
<th mat-header-cell *matHeaderCellDef>Produit</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-mix-materials-form-combo-box
[mixMaterial]="mixMaterial"
[mix]="mix"
[mixMaterials]="mixMaterials"
[control]="getControls(mixMaterial.position).materialId"
[materials]="allMaterials"
[position]="mixMaterial.position">
</cre-mix-materials-form-combo-box>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-input [control]="getControls(mixMaterial.position).quantity" type="number"></cre-input>
</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>
</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-warn-button (click)="removeRow(mixMaterial)" [disabled]="mixMaterials.length === 1">Retirer
</cre-warn-button>
</td>
</ng-container>
</cre-table>
</div>

View File

@ -0,0 +1,255 @@
import {AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild, ViewChildren} from '@angular/core'
import {CreTable} from '../../shared/components/tables/tables'
import {Mix, MixMaterialDto, mixMaterialsToMixMaterialsDto, 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'
import {UNIT_MILLILITER} from '../../shared/units'
@Component({
selector: 'cre-mix-materials-form-combo-box',
templateUrl: 'materials-form-combo-box.html'
})
export class MixMaterialsFormComboBox implements OnInit {
@ViewChild(CreComboBoxComponent) comboBox: CreComboBoxComponent
@Input() mixMaterial: MixMaterialDto
@Input() mix: Mix | null
@Input() mixMaterials: MixMaterialDto[]
@Input() control: FormControl
@Input() materials: Material[]
@Input() position: number
entries: CreInputEntry[]
ngOnInit() {
this.entries = this.filterMaterials()
}
updateEntries() {
this.entries = this.filterMaterials()
this.comboBox.reloadEntries()
}
private filterMaterials(): CreInputEntry[] {
return this.materials
.filter(material => {
if (this.mix && this.mix.mixType.material.id === material.id) {
return false
}
// Prevent use of percents in first position
if (material.materialType.usePercentages && this.mixMaterial.position <= 1) {
return false
}
if (this.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))
}
}
@Component({
selector: 'cre-mix-materials-form',
templateUrl: 'materials-form.html'
})
export class MixMaterialsForm implements AfterViewInit, OnDestroy {
@ViewChild(CreTable) table: CreTable<MixMaterialDto>
@ViewChildren(MixMaterialsFormComboBox) comboBoxes: MixMaterialsFormComboBox[]
@Input() materials: Observable<Material[]>
@Input() mix: Mix | null
mixMaterials: MixMaterialDto[] = []
columns = ['position', 'positionButtons', 'material', 'quantity', 'units', 'endButton']
allMaterials: Material[]
private _controls: ControlsByPosition[] = []
private _destroy$ = new Subject<boolean>()
constructor(
private accountService: AccountService,
private cdRef: ChangeDetectorRef
) {
}
ngAfterViewInit() {
this.materials.subscribe({
next: materials => {
this.allMaterials = materials
if (!this.mix) {
this.addRow()
} else {
mixMaterialsToMixMaterialsDto(this.mix).forEach(x => this.insertRow(x))
}
this.table.renderRows()
this.cdRef.detectChanges()
}
})
}
ngOnDestroy() {
this._destroy$.next(true)
this._destroy$.complete()
}
addRow() {
const mixMaterial = new MixMaterialDto(null, 0, false, this.nextPosition, UNIT_MILLILITER)
this.insertRow(mixMaterial)
this.table.renderRows()
}
insertRow(mixMaterial: MixMaterialDto) {
const materialIdControl = new FormControl(mixMaterial.materialId, Validators.required)
const quantityControl = new FormControl(mixMaterial.quantity, Validators.required)
const unitsControl = new FormControl(mixMaterial.units, Validators.required)
materialIdControl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe({
next: materialId => {
mixMaterial.materialId = materialId
this.refreshAvailableMaterials()
this.cdRef.detectChanges()
}
})
this.mixMaterials.push(mixMaterial)
this._controls.push({
position: mixMaterial.position,
controls: {
materialId: materialIdControl,
quantity: quantityControl,
units: unitsControl
}
})
}
removeRow(mixMaterial: MixMaterialDto) {
this.mixMaterials = this.mixMaterials.filter(x => x.position !== mixMaterial.position)
this._controls = this._controls.filter(x => x.position !== mixMaterial.position)
for (let position = mixMaterial.position + 1; position < this.mixMaterials.length; position++) {
this.updatePosition(this.getMixMaterialByPosition(position), position - 1, false)
}
}
updatePosition(mixMaterial: MixMaterialDto, newPosition: number, switchPositions = true) {
const currentPosition = mixMaterial.position
const currentControls = this.getControlsByPosition(currentPosition)
// Update before current to prevent position conflicts
if (switchPositions) {
this.updatePosition(this.getMixMaterialByPosition(newPosition), currentPosition, false)
}
mixMaterial.position = newPosition
currentControls.position = newPosition
this.sortTable()
this.refreshAvailableMaterials()
}
getControls(position: number): MixMaterialControls {
return this.getControlsByPosition(position).controls
}
areUnitsPercents(mixMaterial: MixMaterialDto): boolean {
if (!mixMaterial) {
return false
}
return mixMaterial.materialId ? this.allMaterials?.filter(x => x.id === mixMaterial.materialId)[0].materialType.usePercentages : false
}
isDecreasePositionButtonDisabled(mixMaterial: MixMaterialDto): boolean {
return mixMaterial.position <= 2 && this.areUnitsPercents(mixMaterial)
}
isIncreasePositionButtonDisabled(mixMaterial: MixMaterialDto): boolean {
if (mixMaterial.position === this.mixMaterials.length) {
return true
}
if (mixMaterial.position > 1) {
return false
}
const nextMixMaterial = this.getMixMaterialByPosition(mixMaterial.position + 1)
return this.areUnitsPercents(nextMixMaterial)
}
get hasMaterialEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
}
get materialCount(): number {
return this.allMaterials ? this.allMaterials.length : 0
}
get updatedMixMaterials(): MixMaterialDto[] {
const updatedMixMaterials: MixMaterialDto[] = []
this.mixMaterials.forEach(mixMaterial => {
const controls = this.getControlsByPosition(mixMaterial.position).controls
updatedMixMaterials.push({
materialId: controls.materialId.value,
quantity: controls.quantity.value,
position: mixMaterial.position,
units: controls.units.value,
isPercents: this.areUnitsPercents(mixMaterial)
})
})
return updatedMixMaterials
}
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 {
return this.mixMaterials.length + 1
}
private getMixMaterialByPosition(position: number): MixMaterialDto {
return this.mixMaterials.filter(x => x.position === position)[0]
}
private getControlsByPosition(position: number): ControlsByPosition {
return this._controls.filter(control => control.position === position)[0]
}
private refreshAvailableMaterials() {
this.comboBoxes.forEach(x => x.updateEntries())
}
private sortTable() {
this.mixMaterials = sortMixMaterialsDto(this.mixMaterials)
this.table.renderRows()
}
}
interface MixMaterialControls {
materialId: FormControl
quantity: FormControl
units: FormControl
}
interface ControlsByPosition {
position: number
controls: MixMaterialControls
}

View File

@ -0,0 +1,171 @@
import {Component, Directive, Input, OnInit, ViewChild} from '@angular/core'
import {SubscribingComponent} from '../../shared/components/subscribing.component'
import {Mix, 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'
import {MixSaveDto, MixService, MixUpdateDto} from '../services/mix.service'
@Directive()
abstract class _BaseMixPage extends SubscribingComponent {
materialTypes$ = this.materialTypeService.all
materials$: Observable<Material[]>
private _recipe: Recipe | null
constructor(
protected mixService: MixService,
private recipeService: RecipeService,
private materialTypeService: MaterialTypeService,
private materialService: MaterialService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
this.fetchRecipe()
}
private fetchRecipe() {
const recipeId = this.urlUtils.parseIntUrlParam('recipeId')
this.subscribe(
this.recipeService.getById(recipeId),
recipe => this.recipe = recipe
)
}
set recipe(recipe: Recipe) {
this._recipe = recipe
this.materials$ = this.materialService.getAllForMixCreation(recipe.id)
}
get recipe(): Recipe {
return this._recipe
}
abstract submit(dto: MixSaveDto)
}
@Component({
selector: 'cre-mix-add',
templateUrl: 'add.html'
})
export class MixAdd extends _BaseMixPage {
submit(dto: MixSaveDto) {
this.subscribeAndNavigate(
this.mixService.saveDto(dto),
`/color/edit/${this.recipe.id}`
)
}
}
@Component({
selector: 'cre-mix-edit',
templateUrl: 'edit.html'
})
export class MixEdit extends _BaseMixPage {
mix: Mix
ngOnInit() {
super.ngOnInit()
this.fetchMix()
}
private fetchMix() {
const mixId = this.urlUtils.parseIntUrlParam('id')
this.subscribe(
this.mixService.getById(mixId),
mix => this.mix = mix
)
}
submit(dto: MixSaveDto) {
this.subscribeAndNavigate(
this.mixService.updateDto({...dto, id: this.mix.id}),
`/color/edit/${this.recipe.id}`
)
}
}
@Component({
selector: 'cre-mix-info-form',
templateUrl: 'info-form.html'
})
export class MixInfoForm implements OnInit {
@ViewChild(CreForm) form: CreForm
@Input() recipe: Recipe
@Input() mix: Mix | null
@Input() materialTypes: Observable<MaterialType[]>
materialTypeEntries: Observable<CreInputEntry[]>
controls: any
ngOnInit() {
this.materialTypeEntries = this.materialTypes.pipe(
map(materialTypes => {
return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))
})
)
this.controls = {
name: new FormControl(this.mix?.mixType.name, Validators.required),
materialType: new FormControl(this.mix?.mixType.material.materialType.id, Validators.required)
}
}
get mixName(): string {
return this.controls.name.value
}
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() mix: Mix | null
@Input() materialTypes: Observable<MaterialType[]>
@Input() materials: Observable<Material[]>
get formValues(): MixSaveDto {
return {
name: this.infoForm.mixName,
recipeId: this.recipe.id,
materialTypeId: this.infoForm.mixMaterialTypeId,
mixMaterials: this.mixMaterialsForm.updatedMixMaterials
}
}
get valid(): boolean {
return this.infoForm?.valid && this.mixMaterialsForm?.valid
}
}

View File

@ -0,0 +1,37 @@
import {NgModule} from '@angular/core'
import {RouterModule, Routes} from '@angular/router'
import {CreRecipeExplore} from './explore'
import {RecipeAdd, RecipeEdit} from './recipes'
import {RecipeList} from './list'
import {MixAdd, MixEdit} from './mix/mix'
const routes: Routes = [{
path: 'list',
component: RecipeList
}, {
path: 'add',
component: RecipeAdd
}, {
path: 'edit/:id',
component: RecipeEdit
}, {
path: 'add/mix/:recipeId',
component: MixAdd
}, {
path: 'edit/mix/:recipeId/:id',
component: MixEdit
}, {
path: 'explore/:id',
component: CreRecipeExplore
}, {
path: '',
pathMatch: 'full',
redirectTo: 'list'
}]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class RecipesRoutingModule {
}

View File

@ -0,0 +1,62 @@
import {NgModule} from '@angular/core'
import {RecipesRoutingModule} from './recipes-routing.module'
import {SharedModule} from '../shared/shared.module'
import {MatExpansionModule} from '@angular/material/expansion'
import {FormsModule} from '@angular/forms'
import {CreRecipeExplore} from './explore'
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 {UnitSelectorComponent} from './components/unit-selector/unit-selector.component'
import {ImagesEditorComponent} from './components/images-editor/images-editor.component'
import {MixesCardComponent} from './components/mixes-card/mixes-card.component'
import {MatSortModule} from '@angular/material/sort'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {RecipeAdd, RecipeEdit, RecipeForm} from './recipes'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {RecipeList} from './list'
import {MixAdd, MixEdit, MixForm, MixInfoForm} from './mix/mix'
import {CreTablesModule} from '../shared/components/tables/tables.module'
import {MixMaterialsForm, MixMaterialsFormComboBox} from './mix/materials-form'
@NgModule({
declarations: [
CreRecipeExplore,
RecipeInfoComponent,
MixTableComponent,
StepListComponent,
StepTableComponent,
UnitSelectorComponent,
ImagesEditorComponent,
MixesCardComponent,
RecipeForm,
RecipeAdd,
RecipeEdit,
RecipeList,
MixAdd,
MixEdit,
MixForm,
MixInfoForm,
MixMaterialsForm,
MixMaterialsFormComboBox
],
exports: [
UnitSelectorComponent
],
imports: [
RecipesRoutingModule,
SharedModule,
MatExpansionModule,
FormsModule,
MatSortModule,
CreInputsModule,
CreButtonsModule,
CreActionBarModule,
CreTablesModule
]
})
export class RecipesModule {
}

View File

@ -0,0 +1,30 @@
.recipe-wrapper > section
margin: 0 3rem 3rem
cre-form
margin-top: 0 !important
mat-expansion-panel
width: 60rem
margin: 20px auto
.button-add
margin-top: .8rem
.recipe-color-circle
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)
.recipe-content > div
margin: 0 3rem 3rem
cre-table
.mat-column-name,
.mat-column-color,
.mat-column-iconNotApproved
width: 5em
.mat-column-description
width: 50em
.mat-column-sample
width: 10em

View File

@ -0,0 +1,211 @@
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component';
import {Observable, Subject} from 'rxjs';
import {CreInputEntry} from '../shared/components/inputs/inputs';
import {map, tap} from 'rxjs/operators';
import {RecipeService} from './services/recipe.service';
import {CompanyService} from '../company/service/company.service';
import {AppState} from '../shared/app-state';
import {ErrorHandler, ErrorService} from '../shared/service/error.service';
import {ActivatedRoute, Router} from '@angular/router';
import {FormControl, Validators} from '@angular/forms';
import {Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core';
import {Recipe, recipeMixCount, RecipeStep, recipeStepCount} from '../shared/model/recipe.model';
import {AccountService} from '../accounts/services/account.service';
import {Permission} from '../shared/model/user';
import {AlertService} from '../shared/service/alert.service';
import {GroupService} from '../groups/services/group.service';
import {StepTableComponent} from './components/step-table/step-table.component';
import {anyMap} from '../shared/utils/map.utils';
import {CreForm, ICreForm} from '../shared/components/forms/forms';
@Component({
selector: 'recipe-form',
templateUrl: 'form.html',
styleUrls: ['recipes.sass'],
encapsulation: ViewEncapsulation.None
})
export class RecipeForm extends SubscribingComponent {
@ViewChild(CreForm) creForm: ICreForm
@Input() recipe: Recipe | null
@Output() submitForm = new EventEmitter<Recipe>();
controls: any
companyEntries$: Observable<CreInputEntry[]>
hasCompanies = true
constructor(
private companyService: CompanyService,
private accountService: AccountService,
errorService: ErrorService,
activatedRoute: ActivatedRoute,
router: Router,
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit();
this.fetchCompanies()
this.controls = {
name: new FormControl(this.recipe?.name, Validators.required),
description: new FormControl(this.recipe?.description, Validators.required),
color: new FormControl(this.recipe?.color ?? '#ffffff', Validators.required),
gloss: new FormControl(this.recipe?.gloss ?? 0, Validators.compose([Validators.required, Validators.min(0), Validators.max(100)])),
sample: new FormControl(this.recipe?.sample, Validators.compose([Validators.required, Validators.min(0)])),
approbationDate: new FormControl(this.recipe?.approbationDate),
remark: new FormControl(this.recipe?.remark),
company: new FormControl({value: this.recipe?.company.id, disabled: !!this.recipe}, Validators.required)
}
}
private fetchCompanies() {
this.companyEntries$ = this.companyService.all.pipe(
tap(companies => this.hasCompanies = companies.length > 0),
map(companies => companies.map(c => new CreInputEntry(c.id, c.name))),
)
}
submit() {
this.submitForm.emit(this.updatedRecipe)
}
get updatedRecipe(): Recipe {
return {
...this.recipe,
name: this.controls.name.value,
description: this.controls.description.value,
color: this.controls.color.value,
gloss: this.controls.gloss.value,
sample: this.controls.sample.value,
approbationDate: this.controls.approbationDate.value,
remark: this.controls.remark.value,
company: this.controls.company.value,
}
}
get hasCompanyEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
}
@Component({
selector: 'cre-recipe-add',
templateUrl: 'add.html'
})
export class RecipeAdd extends ErrorHandlingComponent {
errorHandlers = [{
filter: error => error.type === `exists-recipe-company-name`,
messageProducer: error => `Une couleur avec le nom ${error.name} existe déjà pour la bannière ${error.company}`
}]
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = 'Nouvelle couleur'
}
submit(recipe: Recipe) {
this.subscribe(
this.recipeService.save(recipe),
recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`)
)
}
}
@Component({
selector: 'cre-recipe-edit',
templateUrl: 'edit.html',
styleUrls: ['recipes.sass']
})
export class RecipeEdit extends ErrorHandlingComponent {
@ViewChild(StepTableComponent) stepTable: StepTableComponent
@ViewChild(RecipeForm) form: RecipeForm
recipe: Recipe
groups$ = this.groupService.all
units$ = new Subject<string>()
errorHandlers: ErrorHandler[] = [{
filter: error => error.type === 'notfound-recipe-id',
consumer: _ => this.urlUtils.navigateTo('/color/list')
}]
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
private groupService: GroupService,
private appState: AppState,
private alertService: AlertService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.fetchRecipe()
}
private fetchRecipe() {
const recipeId = this.urlUtils.parseIntUrlParam('id')
this.subscribe(
this.recipeService.getById(recipeId),
recipe => {
this.recipe = recipe
this.appState.title = `${recipe.name} (Modifications)`
if (recipeMixCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucun mélange dans cette recette')
}
if (recipeStepCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucune étape dans cette recette')
}
},
true,
1
)
}
changeUnits(unit: string) {
this.units$.next(unit)
}
submit() {
const recipe = this.form.updatedRecipe
const steps = this.stepTable.mappedUpdatedSteps
if (!this.stepsPositionsAreValid(steps)) {
this.alertService.pushError('Les étapes ne peuvent pas avoir une position inférieure à 1')
return
}
this.subscribeAndNavigate(
this.recipeService.update(recipe, steps),
'/color/list'
)
}
delete() {
this.subscribeAndNavigate(
this.recipeService.delete(this.recipe.id),
'/color/list'
)
}
get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id
}
private stepsPositionsAreValid(steps: Map<number, RecipeStep[]>): boolean {
return !anyMap(steps, (groupId, steps) => !!steps.find(s => s.position === 0))
}
}

View File

@ -21,8 +21,17 @@ export class MixService {
return this.api.get<Mix>(`/recipe/mix/${id}`) return this.api.get<Mix>(`/recipe/mix/${id}`)
} }
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> { saveDto(dto: MixSaveDto): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units)) return this.saveWithUnits(
dto.name,
dto.recipeId,
dto.materialTypeId,
dto.mixMaterials,
)
}
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials))
} }
save(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> { save(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
@ -36,8 +45,17 @@ export class MixService {
return this.api.post('/recipe/mix', body) return this.api.post('/recipe/mix', body)
} }
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> { updateDto(dto: MixUpdateDto): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units)) return this.updateWithUnits(
dto.id,
dto.name,
dto.materialTypeId,
dto.mixMaterials
)
}
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials))
} }
update(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> { update(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
@ -56,11 +74,12 @@ export class MixService {
return this.api.delete(`/recipe/mix/${id}`) return this.api.delete(`/recipe/mix/${id}`)
} }
private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[], units: string): MixMaterialDto[] { private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[]): MixMaterialDto[] {
return mixMaterials.map(m => { return mixMaterials.map(mixMaterial => ({
m.quantity = convertMixMaterialQuantity(m, units, UNIT_MILLILITER) ...mixMaterial,
return m quantity: convertMixMaterialQuantity(mixMaterial, UNIT_MILLILITER),
}) units: UNIT_MILLILITER
}))
} }
private appendMixMaterialsToBody(mixMaterials: MixMaterialDto[], body: any) { private appendMixMaterialsToBody(mixMaterials: MixMaterialDto[], body: any) {
@ -74,3 +93,17 @@ export class MixService {
} }
} }
export interface MixSaveDto {
name: string
recipeId: number
materialTypeId: number
mixMaterials: MixMaterialDto[]
}
export interface MixUpdateDto {
id: number
name: string
materialTypeId: number
mixMaterials: MixMaterialDto[]
}

View File

@ -21,16 +21,16 @@ export class RecipeService {
return this.api.get<Recipe[]>(`/recipe?name=${name}`) return this.api.get<Recipe[]>(`/recipe?name=${name}`)
} }
get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> { get allByCompany(): Observable<Map<number, Recipe[]>> {
return this.all.pipe(map(recipes => { return this.all.pipe(map(recipes => {
const mapped = [] const map = new Map<number, Recipe[]>()
recipes.forEach(r => { recipes.forEach(r => {
if (!mapped[r.company.id]) { if (!map.has(r.company.id)) {
mapped[r.company.id] = {company: r.company.name, recipes: []} map.set(r.company.id, [])
} }
mapped[r.company.id].recipes.push(r) map.get(r.company.id).push(r)
}) })
return mapped.filter(e => e != null) // Filter to remove empty elements in the array that appears for some reason return map
})) }))
} }
@ -38,20 +38,19 @@ export class RecipeService {
return this.api.get<Recipe>(`/recipe/${id}`) return this.api.get<Recipe>(`/recipe/${id}`)
} }
save(name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, companyId: number): Observable<Recipe> { save(recipe: Recipe): Observable<Recipe> {
const body = {name, description, color, gloss, sample, remark, companyId} const body = {
if (approbationDate) { ...recipe,
// @ts-ignore companyId: recipe.company
body.approbationDate = approbationDate
} }
return this.api.post<Recipe>('/recipe', body) return this.api.post<Recipe>('/recipe', body)
} }
update(id: number, name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, steps: Map<number, RecipeStep[]>) { update(recipe: Recipe, steps: Map<number, RecipeStep[]>) {
const body = {id, name, description, color, gloss, sample, remark, steps: []} const body = {
if (approbationDate) { ...recipe,
// @ts-ignore steps: []
body.approbationDate = approbationDate
} }
steps.forEach((groupSteps, groupId) => { steps.forEach((groupSteps, groupId) => {

View File

@ -1,3 +1,3 @@
<div class="d-flex flex-row justify-content-between px-5 py-4"> <div class="d-flex flex-row justify-content-between px-5 py-4" [class.flex-row-reverse]="reverse">
<ng-content select="cre-action-group"></ng-content> <ng-content select="cre-action-group"></ng-content>
</div> </div>

View File

@ -1,4 +1,4 @@
import {Component} from '@angular/core' import {Component, Input} from '@angular/core'
@Component({ @Component({
selector: 'cre-action-group', selector: 'cre-action-group',
@ -11,4 +11,6 @@ export class CreActionGroup {}
selector: 'cre-action-bar', selector: 'cre-action-bar',
templateUrl: 'action-bar.html' templateUrl: 'action-bar.html'
}) })
export class CreActionBar {} export class CreActionBar {
@Input() reverse = false
}

View File

@ -1,3 +1,6 @@
<div> <div class="d-flex flex-column">
<ng-content></ng-content> <div>
<ng-content></ng-content>
</div>
<ng-content select="cre-action-group"></ng-content>
</div> </div>

View File

@ -0,0 +1,3 @@
<div class="alert alert-warning w-50 m-auto text-center">
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,14 @@
import {NgModule} from '@angular/core';
import {WarningAlert} from './alerts';
@NgModule({
declarations: [
WarningAlert
],
exports: [
WarningAlert
],
imports: []
})
export class CreAlertsModule {
}

View File

@ -0,0 +1,10 @@
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'cre-warning-alert',
templateUrl: 'alerts.html',
encapsulation: ViewEncapsulation.None
})
export class WarningAlert {
}

View File

@ -0,0 +1,17 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {ICreForm} from './forms';
@Component({
selector: 'cre-submit-button',
templateUrl: 'submit-button.html'
})
export class CreSubmitButton {
@Input() form: ICreForm
@Input() valid: boolean | null
@Output() submit = new EventEmitter<void>()
get disableButton(): boolean {
return !this.form || !(this.valid ?? this.form.valid)
}
}

View File

@ -1,28 +1,34 @@
import {NgModule} from '@angular/core' import {NgModule} from '@angular/core'
import {CreFormActions, CreFormComponent, CreFormContent, CreFormTitle} from './forms' import {CreFormActions, CreForm, CreFormContent, CreFormTitle} from './forms'
import {MatCardModule} from '@angular/material/card' import {MatCardModule} from '@angular/material/card'
import {CommonModule} from '@angular/common' import {CommonModule} from '@angular/common'
import {MatButtonModule} from '@angular/material/button' import {MatButtonModule} from '@angular/material/button'
import {ReactiveFormsModule} from '@angular/forms' import {ReactiveFormsModule} from '@angular/forms'
import {CreSubmitButton} from './buttons';
import {CreButtonsModule} from '../buttons/buttons.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
CreFormComponent, CreForm,
CreFormTitle, CreFormTitle,
CreFormContent, CreFormContent,
CreFormActions CreFormActions,
CreSubmitButton
], ],
exports: [ exports: [
CreFormComponent, CreForm,
CreFormTitle, CreFormTitle,
CreFormContent, CreFormContent,
CreFormActions CreFormActions,
CreSubmitButton
], ],
imports: [ imports: [
MatCardModule, MatCardModule,
CommonModule, CommonModule,
MatButtonModule, MatButtonModule,
ReactiveFormsModule ReactiveFormsModule,
] CreButtonsModule
]
}) })
export class CreFormsModule {} export class CreFormsModule {
}

View File

@ -1,8 +1,12 @@
cre-form cre-form
display: block display: block
width: max-content
min-width: 50rem
margin-top: 3rem
mat-card mat-card
width: inherit width: inherit
min-width: inherit
cre-form-actions cre-form-actions
display: flex display: flex

View File

@ -1,6 +1,12 @@
import {Component, ContentChild, Directive, Input, OnInit, ViewEncapsulation} from '@angular/core' import {Component, ContentChild, Directive, Input, OnInit, ViewEncapsulation} from '@angular/core'
import {FormBuilder, FormGroup} from '@angular/forms' import {FormBuilder, FormGroup} from '@angular/forms'
export interface ICreForm {
form: FormGroup
valid: boolean
invalid: boolean
}
@Directive({ @Directive({
selector: 'cre-form-title' selector: 'cre-form-title'
}) })
@ -17,7 +23,6 @@ export class CreFormContent {
selector: 'cre-form-actions' selector: 'cre-form-actions'
}) })
export class CreFormActions { export class CreFormActions {
} }
@Component({ @Component({
@ -26,7 +31,7 @@ export class CreFormActions {
styleUrls: ['forms.sass'], styleUrls: ['forms.sass'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CreFormComponent implements OnInit { export class CreForm implements ICreForm, OnInit {
@ContentChild(CreFormActions) formActions: CreFormActions @ContentChild(CreFormActions) formActions: CreFormActions
@Input() formControls: { [key: string]: any } @Input() formControls: { [key: string]: any }
@ -42,7 +47,11 @@ export class CreFormComponent implements OnInit {
} }
get hasActions(): boolean { get hasActions(): boolean {
return this.formActions === true return !!this.formActions
}
get valid(): boolean {
return this.form && this.form.valid
} }
get invalid(): boolean { get invalid(): boolean {

View File

@ -0,0 +1 @@
<cre-accent-button [disabled]="disableButton" (click)="submit.emit()">Enregistrer</cre-accent-button>

View File

@ -66,7 +66,7 @@ export class HeaderComponent extends SubscribingComponent {
} }
get logoUrl(): string { get logoUrl(): string {
return environment.apiUrl + "/file?path=images%2Flogo&mediaType=image/png" return environment.apiUrl + "/config/logo"
} }
set activeLink(link: string) { set activeLink(link: string) {

Some files were not shown because too many files have changed in this diff Show More