develop #5

Merged
william merged 42 commits from develop into master 2021-12-15 00:25:11 -05:00
133 changed files with 2186 additions and 1584 deletions
Showing only changes of commit 23b80daa75 - Show all commits

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -18,3 +18,7 @@ $text-color-primary: white;
$color-accent: map-get($theme-accent, 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 = [{
path: 'color',
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule)
loadChildren: () => import('./modules/recipes/recipes.module').then(m => m.RecipesModule)
}, {
path: 'account',
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 {ErrorService} from './modules/shared/service/error.service'
import {ConfigService} from './modules/shared/service/config.service'
import {Config} from './modules/shared/model/config.model'
import {environment} from '../environments/environment'
@Component({
@ -38,7 +37,7 @@ export class AppComponent extends SubscribingComponent {
online => this.isServerOnline = online
)
this.favIcon.href = environment.apiUrl + "/file?path=images%2Ficon"
this.favIcon.href = environment.apiUrl + "/config/icon"
}
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 {CompanyRoutingModule} from "./company-routing.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: [
CommonModule,
CompanyRoutingModule,
SharedModule
SharedModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
]
})
export class CompanyModule { }

View File

@ -1,7 +1,26 @@
<cre-entity-list
[entities$]="companies$"
[columns]="columns"
[buttons]="buttons"
addLink="/catalog/company/add"
addPermission="EDIT_COMPANIES">
</cre-entity-list>
<cre-action-bar [reverse]="true">
<cre-action-group>
<cre-accent-button routerLink="/catalog/company/add">Ajouter</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<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 {ErrorService} from '../../../shared/service/error.service'
import {AppState} from '../../../shared/app-state'
import {tap} from 'rxjs/operators'
import {AccountService} from '../../../accounts/services/account.service'
@Component({
selector: 'cre-list',
@ -12,18 +14,14 @@ import {AppState} from '../../../shared/app-state'
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
companies$ = this.companyService.all
columns = [
{def: 'name', title: 'Nom', valueFn: c => c.name}
]
buttons = [{
text: 'Modifier',
linkFn: t => `/catalog/company/edit/${t.id}`,
permission: Permission.EDIT_COMPANIES
}]
companies$ = this.companyService.all.pipe(tap(companies => this.companiesEmpty = companies.length <= 0))
companiesEmpty = false
columns = ['name', 'editButton']
constructor(
private companyService: CompanyService,
private accountService: AccountService,
private appState: AppState,
errorService: ErrorService,
router: Router,
@ -32,4 +30,8 @@ export class ListComponent extends ErrorHandlingComponent {
super(errorService, activatedRoute, router)
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 {ApiService} from "../../shared/service/api.service";
import {Observable} from "rxjs";
import {Company} from "../../shared/model/company.model";
import {ApiService} from '../../shared/service/api.service';
import {Observable} from 'rxjs';
import {Company} from '../../shared/model/company.model';
@Injectable({
providedIn: 'root'

View File

@ -11,23 +11,23 @@
<div class="d-flex flex-column" style="gap: 1.5rem">
<cre-config-section *ngIf="!emergencyMode" label="Apparence">
<cre-config-list>
<!-- <cre-image-config-->
<!-- label="Logo"-->
<!-- 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-->
<!-- 'Ctrl+F5')."-->
<!-- [configControl]="getConfigControl(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"-->
<!-- (invalidFormat)="invalidFormatConfirmBox.show()">-->
<!-- </cre-image-config>-->
<cre-image-config
label="Logo"
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
'Ctrl+F5')."
[configControl]="getConfigControl(keys.INSTANCE_LOGO_SET)" previewWidth="170px"
(invalidFormat)="invalidFormatConfirmBox.show()">
</cre-image-config>
<!-- <cre-image-config-->
<!-- label="Icône"-->
<!-- 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-->
<!-- 'Ctrl+F5')."-->
<!-- [configControl]="getConfigControl(keys.INSTANCE_ICON_PATH)" previewWidth="32px"-->
<!-- (invalidFormat)="invalidFormatConfirmBox.show()">-->
<!-- </cre-image-config>-->
<cre-image-config
label="Icône"
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
'Ctrl+F5')."
[configControl]="getConfigControl(keys.INSTANCE_ICON_SET)" previewWidth="32px"
(invalidFormat)="invalidFormatConfirmBox.show()">
</cre-image-config>
</cre-config-list>
</cre-config-section>

View File

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

View File

@ -18,7 +18,7 @@
<div class="image-wrapper d-flex flex-column justify-content-end">
<div>
<img
[src]="updatedImage ? updatedImage : configuredImageUrl"
[src]="updatedImage ? updatedImage : imageUrl"
[attr.width]="previewWidth ? previewWidth : null"
class="mat-elevation-z3"/>
</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 {Config, ConfigControl} from '../shared/model/config.model'
import {SubscribingComponent} from '../shared/components/subscribing.component'
import {ErrorService} from '../shared/service/error.service'
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 {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@ -121,9 +131,9 @@ export class CreImageConfig extends _CreConfigBase {
readFile(file, (content) => this.updatedImage = content)
}
get configuredImageUrl(): string {
return getFileUrl(this.config.content)
get imageUrl(): string {
const path = this.config.key == Config.INSTANCE_ICON_SET ? 'icon' : 'logo'
return getConfiguredImageUrl(path)
}
}

View File

@ -6,6 +6,9 @@ import { ListComponent } from './pages/list/list.component';
import {SharedModule} from "../shared/shared.module";
import { AddComponent } from './pages/add/add.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({
@ -13,7 +16,10 @@ import { EditComponent } from './pages/edit/edit.component';
imports: [
CommonModule,
MaterialTypeRoutingModule,
SharedModule
SharedModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
]
})
export class MaterialTypeModule { }

View File

@ -1,7 +1,40 @@
<cre-entity-list
[entities$]="materialTypes$"
[columns]="columns"
[buttons]="buttons"
addLink="/catalog/materialtype/add"
addPermission="EDIT_MATERIAL_TYPES">
</cre-entity-list>
<cre-action-bar [reverse]="true">
<cre-action-group>
<cre-accent-button routerLink="/catalog/materialtype/add">Ajouter</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<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 {ErrorService} from '../../../shared/service/error.service'
import {AppState} from '../../../shared/app-state'
import {tap} from 'rxjs/operators'
import {AccountService} from '../../../accounts/services/account.service'
@Component({
selector: 'cre-list',
@ -12,23 +14,16 @@ import {AppState} from '../../../shared/app-state'
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
materialTypes$ = this.materialTypeService.all
columns = [
{def: 'name', title: 'Nom', valueFn: t => t.name},
{def: 'prefix', title: 'Préfixe', valueFn: t => t.prefix},
{def: 'usePercentages', title: 'Utilise les pourcentages', valueFn: t => t.usePercentages ? 'Oui' : 'Non'}
]
buttons = [
{
text: 'Modifier',
linkFn: t => `/catalog/materialtype/edit/${t.id}`,
permission: Permission.EDIT_MATERIAL_TYPES,
disabledFn: t => t.systemType
}
]
materialTypes$ = this.materialTypeService.all.pipe(
tap(materialTypes => this.materialTypesEmpty = materialTypes.length <= 0)
)
materialTypesEmpty = false
columns = ['name', 'prefix', 'usePercentages', 'editButton']
constructor(
private materialTypeService: MaterialTypeService,
private accountService: AccountService,
private appState: AppState,
errorService: ErrorService,
router: Router,
@ -37,4 +32,8 @@ export class ListComponent extends ErrorHandlingComponent {
super(errorService, activatedRoute, router)
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 {AddComponent} from './pages/add/add.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 {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({
@ -17,9 +21,13 @@ import {FormsModule} from '@angular/forms'
CommonModule,
MaterialRoutingModule,
SharedModule,
ColorsModule,
RecipesModule,
MatSortModule,
FormsModule
FormsModule,
CreTablesModule,
CreInputsModule,
CreButtonsModule,
CreActionBarModule
]
})
export class MaterialModule {

View File

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

View File

@ -1,8 +1,9 @@
.input-group-append button
border-radius: 0 4px 4px 0
mat-select
margin-top: 4px
.form-control
width: 6rem
.input-group
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 {ActivatedRoute, Router} from '@angular/router'
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 {convertQuantity, UNIT_MILLILITER} from '../../../shared/units'
import {MatSort} from '@angular/material/sort'
import {MatTableDataSource} from '@angular/material/table'
import {MaterialTypeService} from '../../../material-type/service/material-type.service'
import {InventoryService} from '../../service/inventory.service'
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({
selector: 'cre-list',
@ -21,9 +24,10 @@ import {AppState} from '../../../shared/app-state'
export class InventoryComponent extends ErrorHandlingComponent {
@ViewChild(MatSort) sort: MatSort
materials: Material[] | null
materialTypes$ = this.materialTypeService.all
dataSource: MatTableDataSource<Material>
materials: Material[] | null = []
materialTypesEntries$ = this.materialTypeService.all.pipe(map(materialTypes => {
return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))
}))
columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton']
hoveredMaterial: Material | null
@ -31,8 +35,16 @@ export class InventoryComponent extends ErrorHandlingComponent {
units = UNIT_MILLILITER
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(
private materialService: MaterialService,
@ -53,38 +65,25 @@ export class InventoryComponent extends ErrorHandlingComponent {
this.subscribe(
this.materialService.allNotMixType,
materials => {
this.materials = materials
this.dataSource = this.setupDataSource()
},
materials => this.materials = materials,
true,
1
)
}
setupDataSource(): MatTableDataSource<Material> {
this.dataSource = new MatTableDataSource<Material>(this.materials)
this.dataSource.sortingDataAccessor = (material, header) => {
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.subscribe(
this.materialTypeFilterControl.valueChanges,
filter => this.materialTypeFilter = filter
)
this.dataSource.sort = this.sort
return this.dataSource
}
this.subscribe(
this.materialNameFilterControl.valueChanges,
filter => this.materialNameFilter = filter
)
filterDataSource() {
this.dataSource.filter = 'filter'
this.subscribe(
this.hideLowQuantityControl.valueChanges,
filter => this.hideLowQuantity = filter
)
}
isLowQuantity(material: Material) {
@ -92,7 +91,7 @@ export class InventoryComponent extends ErrorHandlingComponent {
}
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 {
@ -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 {
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-content [class.no-action]="!editionMode">
<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 class="image-wrapper">
<img [src]="imageUrl" width="300px"/>

View File

@ -1,5 +1,5 @@
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 {SubscribingComponent} from '../../../shared/components/subscribing.component'
import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units'
@ -60,7 +60,7 @@ export class MixTableComponent extends SubscribingComponent {
this.mixColumns = this.COLUMNS_EDIT
}
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix)
this.mixMaterials = mixMaterialsToMixMaterialsDto(this.mix)
this.subscribe(
this.units$,
@ -191,12 +191,13 @@ export class MixTableComponent extends SubscribingComponent {
materialId: quantity.materialId,
quantity: this.calculateQuantity(index),
isPercents: quantity.isPercents,
position: quantity.position
position: quantity.position,
units: UNIT_MILLILITER
})
}
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
}

View File

@ -3,6 +3,8 @@
<mat-card-title>Mélanges</mat-card-title>
</mat-card-header>
<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">
<cre-mix-table
[class.no-top-margin]="i == 0"

View File

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

View File

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

View File

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

View File

@ -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 {RecipeService} from '../../services/recipe.service'
import {RecipeService} from './services/recipe.service'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {MixMaterialDto, Recipe, recipeMixCount, recipeNoteForGroupId, recipeStepCount} from '../../../shared/model/recipe.model'
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
import {
MixMaterialDto,
Recipe,
recipeMixCount,
recipeNoteForGroupId,
recipeStepCount
} from '../shared/model/recipe.model'
import {Observable, Subject} from 'rxjs'
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
import {GlobalAlertHandlerComponent} from '../../../shared/components/global-alert-handler/global-alert-handler.component'
import {InventoryService} from '../../../material/service/inventory.service'
import {ConfirmBoxComponent} from '../../../shared/components/confirm-box/confirm-box.component'
import {GroupService} from '../../../groups/services/group.service'
import {AppState} from '../../../shared/app-state'
import {AccountService} from '../../../accounts/services/account.service'
import {Permission} from '../../../shared/model/user'
import {ErrorHandler, ErrorService} from '../shared/service/error.service'
import {AlertService} from '../shared/service/alert.service'
import {GlobalAlertHandlerComponent} from '../shared/components/global-alert-handler/global-alert-handler.component'
import {InventoryService} from '../material/service/inventory.service'
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
import {GroupService} from '../groups/services/group.service'
import {AppState} from '../shared/app-state'
import {AccountService} from '../accounts/services/account.service'
import {Permission} from '../shared/model/user'
import {FormControl} from '@angular/forms';
import {map} from 'rxjs/operators';
import {CreInputEntry} from '../shared/components/inputs/inputs';
@Component({
selector: 'cre-explore',
templateUrl: './explore.component.html',
styleUrls: ['./explore.component.sass']
selector: 'cre-recipe-explore',
templateUrl: './explore.html',
styleUrls: ['./recipes.sass']
})
export class ExploreComponent extends ErrorHandlingComponent {
recipe: Recipe | null
groups$ = this.groupService.all
export class CreRecipeExplore extends ErrorHandlingComponent {
deductErrorBody = {}
units$ = new Subject<string>()
selectedGroupId: number | null
@ -33,6 +40,12 @@ export class ExploreComponent extends ErrorHandlingComponent {
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[] = [{
filter: error => error.type === 'notfound-recipe-id',
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'
}]
private _recipe: Recipe | null
private _notePlaceholder = !this.canEditRecipesPublicData ? 'N/A' : ''
constructor(
private recipeService: RecipeService,
private inventoryService: InventoryService,
@ -62,18 +78,30 @@ export class ExploreComponent extends ErrorHandlingComponent {
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.recipeService,
id,
r => {
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')
}
}
recipeId,
recipe => this.recipe = recipe
)
}
@ -128,11 +156,24 @@ export class ExploreComponent extends ErrorHandlingComponent {
subscribeDeductMix(observable: Observable<any>) {
this.subscribe(
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
)
}
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 {
return this.appState.authenticatedUser.group?.id
}
@ -141,11 +182,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
if (!this.groupsNote.has(this.selectedGroupId)) {
this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId))
}
return this.groupsNote.get(this.selectedGroupId)
}
set selectedGroupNote(value: string) {
this.groupsNote.set(this.selectedGroupId, value)
return this.groupsNote.get(this.selectedGroupId) ?? this._notePlaceholder
}
get canEditRecipesPublicData(): boolean {
@ -160,7 +197,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
})
this.groupsNote.forEach((content, groupId) => {
updatedNotes.set(groupId, content)
if (content) {
updatedNotes.set(groupId, content)
}
})
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">
<mat-form-field>
<mat-label>Recherche</mat-label>
<input
matInput
type="text"
[(ngModel)]="searchQuery"
(keyup)="searchRecipes()"/>
<button
mat-button
*ngIf="searchQuery"
matSuffix
mat-icon-button
(click)="searchQuery=''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<div class="button-add">
<button *ngIf="hasEditPermission" mat-raised-button color="accent" routerLink="/color/add">Ajouter</button>
</div>
<cre-action-bar>
<cre-action-group>
<cre-input label="Recherche" [control]="searchControl"></cre-input>
</cre-action-group>
<cre-action-group>
<cre-accent-button *ngIf="hasEditPermission" routerLink="/color/add">Ajouter</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div *ngIf="!loading">
<cre-warning-alert *ngIf="companies.length === 0">
<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>
<cre-warning-alert *ngIf="companies.length > 0 && recipes.size === 0">
<p>Il n'y a actuellement aucune recette enregistrée dans le système.</p>
<p *ngIf="hasEditPermission">Vous pouvez en créer une <b><a routerLink="/color/add">ici</a></b>.</p>
</cre-warning-alert>
</div>
<mat-expansion-panel
class="table-title"
*ngFor="let companyRecipes of recipes"
[hidden]="isCompanyHidden(companyRecipes.recipes)"
*ngFor="let company of companies"
[hidden]="isCompanyHidden(company)"
[expanded]="panelForcedExpanded">
<mat-expansion-panel-header>
<mat-panel-title>
{{companyRecipes.company}}
{{company.name}}
</mat-panel-title>
</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>
<ng-template
#recipeTableTemplate
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 -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
@ -85,19 +89,16 @@
<!-- Buttons -->
<ng-container matColumnDef="buttonView">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe">
<button mat-flat-button color="accent" routerLink="/color/explore/{{recipe.id}}">Voir</button>
<td mat-cell *matCellDef="let recipe; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/explore/{{recipe.id}}">Voir</cre-accent-button>
</td>
</ng-container>
<ng-container matColumnDef="buttonEdit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe" [class.disabled]="!hasEditPermission">
<button mat-flat-button color="accent" routerLink="/color/edit/{{recipe.id}}">Modifier</button>
<td mat-cell *matCellDef="let recipe; let i = index" [class.disabled]="!hasEditPermission">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/edit/{{recipe.id}}">Modifier</cre-accent-button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let recipe; columns: tableCols" [hidden]="hiddenRecipes[recipe.id]"></tr>
</table>
</cre-table>
</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}`)
}
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
saveDto(dto: MixSaveDto): Observable<void> {
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> {
@ -36,8 +45,17 @@ export class MixService {
return this.api.post('/recipe/mix', body)
}
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
updateDto(dto: MixUpdateDto): Observable<void> {
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> {
@ -56,11 +74,12 @@ export class MixService {
return this.api.delete(`/recipe/mix/${id}`)
}
private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[], units: string): MixMaterialDto[] {
return mixMaterials.map(m => {
m.quantity = convertMixMaterialQuantity(m, units, UNIT_MILLILITER)
return m
})
private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[]): MixMaterialDto[] {
return mixMaterials.map(mixMaterial => ({
...mixMaterial,
quantity: convertMixMaterialQuantity(mixMaterial, UNIT_MILLILITER),
units: UNIT_MILLILITER
}))
}
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}`)
}
get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> {
get allByCompany(): Observable<Map<number, Recipe[]>> {
return this.all.pipe(map(recipes => {
const mapped = []
const map = new Map<number, Recipe[]>()
recipes.forEach(r => {
if (!mapped[r.company.id]) {
mapped[r.company.id] = {company: r.company.name, recipes: []}
if (!map.has(r.company.id)) {
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}`)
}
save(name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, companyId: number): Observable<Recipe> {
const body = {name, description, color, gloss, sample, remark, companyId}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
save(recipe: Recipe): Observable<Recipe> {
const body = {
...recipe,
companyId: recipe.company
}
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[]>) {
const body = {id, name, description, color, gloss, sample, remark, steps: []}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
update(recipe: Recipe, steps: Map<number, RecipeStep[]>) {
const body = {
...recipe,
steps: []
}
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>
</div>

View File

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

View File

@ -1,3 +1,6 @@
<div>
<ng-content></ng-content>
<div class="d-flex flex-column">
<div>
<ng-content></ng-content>
</div>
<ng-content select="cre-action-group"></ng-content>
</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 {CreFormActions, CreFormComponent, CreFormContent, CreFormTitle} from './forms'
import {CreFormActions, CreForm, CreFormContent, CreFormTitle} from './forms'
import {MatCardModule} from '@angular/material/card'
import {CommonModule} from '@angular/common'
import {MatButtonModule} from '@angular/material/button'
import {ReactiveFormsModule} from '@angular/forms'
import {CreSubmitButton} from './buttons';
import {CreButtonsModule} from '../buttons/buttons.module';
@NgModule({
declarations: [
CreFormComponent,
CreForm,
CreFormTitle,
CreFormContent,
CreFormActions
CreFormActions,
CreSubmitButton
],
exports: [
CreFormComponent,
CreForm,
CreFormTitle,
CreFormContent,
CreFormActions
CreFormActions,
CreSubmitButton
],
imports: [
MatCardModule,
CommonModule,
MatButtonModule,
ReactiveFormsModule
]
imports: [
MatCardModule,
CommonModule,
MatButtonModule,
ReactiveFormsModule,
CreButtonsModule
]
})
export class CreFormsModule {}
export class CreFormsModule {
}

View File

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

View File

@ -1,6 +1,12 @@
import {Component, ContentChild, Directive, Input, OnInit, ViewEncapsulation} from '@angular/core'
import {FormBuilder, FormGroup} from '@angular/forms'
export interface ICreForm {
form: FormGroup
valid: boolean
invalid: boolean
}
@Directive({
selector: 'cre-form-title'
})
@ -17,7 +23,6 @@ export class CreFormContent {
selector: 'cre-form-actions'
})
export class CreFormActions {
}
@Component({
@ -26,7 +31,7 @@ export class CreFormActions {
styleUrls: ['forms.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreFormComponent implements OnInit {
export class CreForm implements ICreForm, OnInit {
@ContentChild(CreFormActions) formActions: CreFormActions
@Input() formControls: { [key: string]: any }
@ -42,7 +47,11 @@ export class CreFormComponent implements OnInit {
}
get hasActions(): boolean {
return this.formActions === true
return !!this.formActions
}
get valid(): boolean {
return this.form && this.form.valid
}
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 {
return environment.apiUrl + "/file?path=images%2Flogo&mediaType=image/png"
return environment.apiUrl + "/config/logo"
}
set activeLink(link: string) {

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