Compare commits
48 Commits
release/2.
...
master
Author | SHA1 | Date |
---|---|---|
William Nolin | 3c630914b0 | |
William Nolin | c0507ec11f | |
FyloZ | 09e89e68bb | |
FyloZ | cc24423389 | |
FyloZ | 6c8d35560e | |
FyloZ | af3b8eee8c | |
FyloZ | f21157d283 | |
William Nolin | 0960eaa507 | |
FyloZ | f04a3c7ba3 | |
FyloZ | 82cb974d27 | |
FyloZ | d239d37e66 | |
William Nolin | 30f8c44a54 | |
William Nolin | 24a713160a | |
FyloZ | be592a22f5 | |
William Nolin | cb7f38b46b | |
William Nolin | 07b410d053 | |
FyloZ | 019b20fd02 | |
FyloZ | 9deecb3cbd | |
William Nolin | 23b80daa75 | |
FyloZ | b6f687e0f1 | |
FyloZ | 2351ecd258 | |
FyloZ | 6ba1073004 | |
FyloZ | 3136f2e1d6 | |
FyloZ | f644edae21 | |
FyloZ | 5b564460d4 | |
FyloZ | f37363e457 | |
FyloZ | 75f356cc5a | |
FyloZ | e18fe04f36 | |
William Nolin | 3a5f90b8c5 | |
FyloZ | a8d13cc1fb | |
FyloZ | f564c37b15 | |
FyloZ | 4ccdd090c1 | |
FyloZ | ba360529cc | |
William Nolin | a0d15b57fb | |
FyloZ | e332fee3ba | |
William Nolin | 6a5f74016f | |
FyloZ | 971e3dcb3c | |
William Nolin | c3122c2e3e | |
William Nolin | cb51e66d11 | |
William Nolin | a1baa334ed | |
William Nolin | c21dd8d0f9 | |
William Nolin | a96062a91f | |
William Nolin | 521db72f5e | |
William Nolin | c35635c596 | |
William Nolin | 73bb2c3bce | |
William Nolin | 951f9fcb87 | |
William Nolin | ae5dac1040 | |
William Nolin | d64aaf8201 |
51
.drone.yml
51
.drone.yml
|
@ -1,12 +1,13 @@
|
|||
---
|
||||
global-variables:
|
||||
release: &release ${DRONE_BRANCH##**/}
|
||||
release: &release ${DRONE_TAG}
|
||||
environment: &environment
|
||||
CRE_REGISTRY_IMAGE: registry.fyloz.dev:5443/colorrecipesexplorer/frontend
|
||||
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/frontend
|
||||
CRE_PORT: 9102
|
||||
CRE_RELEASE: *release
|
||||
alpine-image: &alpine-image alpine:latest
|
||||
docker-registry-repo: &docker-registry-repo registry.fyloz.dev:5443/colorrecipesexplorer/frontend
|
||||
docker-registry: &docker-registry registry.fyloz.dev
|
||||
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/frontend
|
||||
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
@ -21,6 +22,9 @@ steps:
|
|||
- echo -n "latest" > .tags
|
||||
when:
|
||||
branch: develop
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: set-docker-tags-release
|
||||
image: *alpine-image
|
||||
|
@ -29,18 +33,40 @@ steps:
|
|||
commands:
|
||||
- echo -n "latest-release,$CRE_RELEASE" > .tags
|
||||
when:
|
||||
branch: release/**
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: containerize
|
||||
- name: containerize-dev
|
||||
image: plugins/docker
|
||||
environment:
|
||||
<<: *environment
|
||||
settings:
|
||||
repo: *docker-registry-repo
|
||||
registry: *docker-registry
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
branch:
|
||||
- develop
|
||||
- release/**
|
||||
branch: develop
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: containerize-release
|
||||
image: plugins/docker
|
||||
environment:
|
||||
<<: *environment
|
||||
settings:
|
||||
repo: *docker-registry-repo
|
||||
registry: *docker-registry
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy
|
||||
image: alpine:latest
|
||||
|
@ -70,10 +96,5 @@ steps:
|
|||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker pull $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker run -d -p $CRE_PORT:80 --name=$DEPLOY_CONTAINER_NAME $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||
when:
|
||||
branch: release/**
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- develop
|
||||
- release/**
|
||||
- master
|
||||
event:
|
||||
- tag
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/ng-cli-compat",
|
||||
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/component-class-suffix": "off",
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "cre",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/directive-class-suffix": "off",
|
||||
"@angular-eslint/directive-selector": [
|
||||
"off",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "cre",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"off",
|
||||
{
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
"never"
|
||||
],
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"always"
|
||||
],
|
||||
"eqeqeq": [
|
||||
"off",
|
||||
"always"
|
||||
],
|
||||
"import/order": "off",
|
||||
"max-len": [
|
||||
"off",
|
||||
{
|
||||
"code": 140
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -32,6 +32,7 @@ speed-measure-plugin*.json
|
|||
.history/*
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
|
35
angular.json
35
angular.json
|
@ -22,18 +22,27 @@
|
|||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{ "glob": "mdi.svg", "input": "./node_modules/@mdi/angular-material", "output": "./assets"}
|
||||
{
|
||||
"glob": "mdi.svg",
|
||||
"input": "./node_modules/@mdi/angular-material",
|
||||
"output": "./assets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||
"src/custom-theme.scss",
|
||||
"src/styles.sass"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
@ -63,7 +72,8 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": ""
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
|
@ -100,15 +110,11 @@
|
|||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -127,5 +133,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "color-recipes-explorer-frontend"
|
||||
"defaultProject": "color-recipes-explorer-frontend",
|
||||
"cli": {
|
||||
"defaultCollection": "@angular-eslint/schematics"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
67
package.json
67
package.json
|
@ -3,53 +3,66 @@
|
|||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"browser": {
|
||||
"fs": false
|
||||
},
|
||||
"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",
|
||||
"@js-joda/core": "^4.3.1",
|
||||
"@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",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"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/cli": "^11.2.11",
|
||||
"@angular/compiler-cli": "~11.2.10",
|
||||
"@angular/language-service": "~11.2.10",
|
||||
"@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": "^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",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.16.1",
|
||||
"@typescript-eslint/parser": "4.16.1",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-plugin-import": "latest",
|
||||
"eslint-plugin-jsdoc": "latest",
|
||||
"eslint-plugin-prefer-arrow": "latest",
|
||||
"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",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.0.7"
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "~4.3.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {Routes, RouterModule} from '@angular/router';
|
||||
import {NgModule} from '@angular/core'
|
||||
import {RouterModule, Routes} from '@angular/router'
|
||||
import {Login, Logout} from './accounts'
|
||||
|
||||
import {LoginComponent} from './pages/login/login.component';
|
||||
import {LogoutComponent} from "./pages/logout/logout.component";
|
||||
|
||||
const routes: Routes = [{path: 'login', component: LoginComponent}, {path: 'logout', component: LogoutComponent}, {path: '', redirectTo: 'login'}];
|
||||
const routes: Routes = [{
|
||||
path: 'login',
|
||||
component: Login
|
||||
}, {
|
||||
path: 'logout',
|
||||
component: Logout
|
||||
}, {
|
||||
path: '',
|
||||
redirectTo: 'login'
|
||||
}]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {NgModule} from '@angular/core'
|
||||
|
||||
import {AccountsRoutingModule} from './accounts-routing.module';
|
||||
import {LoginComponent} from './pages/login/login.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {LogoutComponent} from './pages/logout/logout.component';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {BrowserModule} from "@angular/platform-browser";
|
||||
import {AccountsRoutingModule} from './accounts-routing.module'
|
||||
import {SharedModule} from '../shared/shared.module'
|
||||
import {Login, Logout} from './accounts'
|
||||
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [LoginComponent, LogoutComponent],
|
||||
declarations: [
|
||||
Login,
|
||||
Logout
|
||||
],
|
||||
imports: [
|
||||
SharedModule,
|
||||
AccountsRoutingModule,
|
||||
CreInputsModule,
|
||||
CreButtonsModule,
|
||||
]
|
||||
})
|
||||
export class AccountsModule {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import {Component, HostListener, ViewChild} from '@angular/core'
|
||||
import {FormControl, Validators} from '@angular/forms'
|
||||
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
|
||||
import {AccountService} from './services/account.service'
|
||||
import {AppState} from '../shared/app-state'
|
||||
import {ErrorHandler, ErrorService} from '../shared/service/error.service'
|
||||
import {ActivatedRoute, Router} from '@angular/router'
|
||||
import {CreForm, ICreForm} from "../shared/components/forms/forms";
|
||||
import {AlertService} from "../shared/service/alert.service";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-login',
|
||||
templateUrl: 'login.html',
|
||||
styles: [
|
||||
'cre-form { min-width: 25rem; margin-top: 50vh; transform: translateY(-70%) }'
|
||||
]
|
||||
})
|
||||
export class Login extends ErrorHandlingComponent {
|
||||
@ViewChild(CreForm) form: ICreForm
|
||||
|
||||
userIdControl = new FormControl(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$'))]))
|
||||
passwordControl = new FormControl(null, Validators.required)
|
||||
|
||||
errorHandlers: ErrorHandler[] = [{
|
||||
filter: error => error.status === 403,
|
||||
messageProducer: () => 'Les identifiants entrés sont invalides'
|
||||
}]
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private alertService: AlertService,
|
||||
private appState: AppState,
|
||||
errorService: ErrorService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(errorService, activatedRoute, router)
|
||||
this.appState.title = 'Connexion'
|
||||
}
|
||||
|
||||
// Allows to send the form by pressing Enter
|
||||
@HostListener('window:keyup.enter', ['$event'])
|
||||
onEnterKeyEvent() {
|
||||
if (this.form.formGroup) {
|
||||
this.submit()
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.subscribeAndNavigate(
|
||||
this.accountService.login(this.userIdControl.value, this.passwordControl.value),
|
||||
'/color/list'
|
||||
)
|
||||
}
|
||||
|
||||
get controls(): { userId: FormControl, password: FormControl } {
|
||||
return {
|
||||
userId: this.userIdControl,
|
||||
password: this.passwordControl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-logout',
|
||||
template: ''
|
||||
})
|
||||
export class Logout extends SubscribingComponent {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private alertService: AlertService,
|
||||
private appState: AppState,
|
||||
errorService: ErrorService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(errorService, activatedRoute, router)
|
||||
this.appState.title = 'Connexion'
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.appState.isAuthenticated) {
|
||||
this.urlUtils.navigateTo('/account/login')
|
||||
}
|
||||
|
||||
this.subscribeAndNavigate(
|
||||
this.accountService.logout(),
|
||||
'/account/login'
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<cre-form #form [formControls]="controls" class="mx-auto">
|
||||
<cre-form-title>Connexion au système</cre-form-title>
|
||||
<cre-form-content>
|
||||
<cre-input
|
||||
[control]="userIdControl"
|
||||
label="Numéro d'utilisateur"
|
||||
icon="account">
|
||||
<ng-template let-errors="errors">
|
||||
<span *ngIf="errors && errors.pattern">Le numéro d'utilisateur doit être un nombre</span>
|
||||
</ng-template>
|
||||
</cre-input>
|
||||
|
||||
<cre-input
|
||||
[control]="passwordControl"
|
||||
type="password"
|
||||
label="Mot de passe"
|
||||
icon="lock">
|
||||
</cre-input>
|
||||
</cre-form-content>
|
||||
<cre-form-actions>
|
||||
<cre-accent-button
|
||||
type="submit"
|
||||
[disabled]="!form.valid"
|
||||
(click)="submit()">
|
||||
Connexion
|
||||
</cre-accent-button>
|
||||
</cre-form-actions>
|
||||
</cre-form>
|
|
@ -1,36 +0,0 @@
|
|||
<form [formGroup]="form">
|
||||
<mat-card class="x-centered y-centered">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Connexion au système</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'utilisateur</mat-label>
|
||||
<input matInput [formControl]="idFormControl" type="text"/>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="idFormControl.invalid">
|
||||
<span *ngIf="idFormControl.errors.required">Un numéro d'utilisateur est requis</span>
|
||||
<span *ngIf="idFormControl.errors.pattern">Le numéro d'utilisateur doit être un nombre</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input matInput [formControl]="passwordFormControl" type="password"/>
|
||||
<mat-icon matSuffix>lock</mat-icon>
|
||||
<mat-error *ngIf="passwordFormControl.invalid">
|
||||
<span *ngIf="passwordFormControl.errors.required">Un mot de passe est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="justify-content-end">
|
||||
<button
|
||||
mat-raised-button
|
||||
type="submit"
|
||||
color="accent"
|
||||
[disabled]="form.invalid"
|
||||
(click)="submit()">
|
||||
Connexion
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
|
@ -1,8 +0,0 @@
|
|||
mat-card
|
||||
width: 25rem
|
||||
|
||||
.alert p
|
||||
margin: 0
|
||||
|
||||
mat-form-field
|
||||
width: 100%
|
|
@ -1,53 +0,0 @@
|
|||
import {Component, OnInit} from '@angular/core'
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'
|
||||
import {AccountService} from '../../services/account.service'
|
||||
import {ActivatedRoute, Router} from '@angular/router'
|
||||
import {ErrorService} from '../../../shared/service/error.service'
|
||||
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
|
||||
import {AppState} from '../../../shared/app-state'
|
||||
|
||||
@Component({
|
||||
selector: 'cre-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.sass']
|
||||
})
|
||||
export class LoginComponent extends ErrorHandlingComponent implements OnInit {
|
||||
form: FormGroup
|
||||
idFormControl: FormControl
|
||||
passwordFormControl: FormControl
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private appState: AppState,
|
||||
errorService: ErrorService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(errorService, activatedRoute, router)
|
||||
this.appState.title = 'Connexion'
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.errorService.activeErrorHandler = this
|
||||
|
||||
if (this.accountService.isLoggedIn()) {
|
||||
this.router.navigate(['/color'])
|
||||
}
|
||||
|
||||
this.idFormControl = this.formBuilder.control(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$'))]))
|
||||
this.passwordFormControl = this.formBuilder.control(null, Validators.required)
|
||||
this.form = this.formBuilder.group({
|
||||
id: this.idFormControl,
|
||||
password: this.passwordFormControl
|
||||
})
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.accountService.login(
|
||||
this.idFormControl.value,
|
||||
this.passwordFormControl.value,
|
||||
() => this.router.navigate(['/color'])
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {AccountService} from "../../services/account.service";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-logout',
|
||||
templateUrl: './logout.component.html',
|
||||
styleUrls: ['./logout.component.sass']
|
||||
})
|
||||
export class LogoutComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.accountService.isLoggedIn()) {
|
||||
this.router.navigate(['/account/login'])
|
||||
}
|
||||
|
||||
this.accountService.logout(() => {
|
||||
this.router.navigate(['/account/login'])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import {Injectable, OnDestroy} from '@angular/core'
|
||||
import {Subject} from 'rxjs'
|
||||
import {Observable, Subject} from 'rxjs'
|
||||
import {take, takeUntil} from 'rxjs/operators'
|
||||
import {AppState} from '../../shared/app-state'
|
||||
import {HttpClient, HttpResponse} from '@angular/common/http'
|
||||
import {environment} from '../../../../environments/environment'
|
||||
import {ApiService} from '../../shared/service/api.service'
|
||||
import {User, Permission} from '../../shared/model/user'
|
||||
import {Permission, User} from '../../shared/model/user'
|
||||
import {ErrorService} from '../../shared/service/error.service'
|
||||
import {globalLoadingWheel} from '../../shared/components/loading-wheel/loading-wheel.component'
|
||||
import {AlertService} from '../../shared/service/alert.service'
|
||||
import {JwtService} from "./jwt.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -20,6 +20,7 @@ export class AccountService implements OnDestroy {
|
|||
private http: HttpClient,
|
||||
private api: ApiService,
|
||||
private appState: AppState,
|
||||
private jwtService: JwtService,
|
||||
private errorService: ErrorService,
|
||||
private alertService: AlertService
|
||||
) {
|
||||
|
@ -30,20 +31,16 @@ export class AccountService implements OnDestroy {
|
|||
this.destroy$.complete()
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.appState.isAuthenticated
|
||||
}
|
||||
|
||||
checkAuthenticationStatus() {
|
||||
if (!this.appState.authenticatedUser) {
|
||||
if (!this.appState.isAuthenticated) {
|
||||
// Try to get current default group user
|
||||
this.http.get<User>(`${environment.apiUrl}/user/current`, {withCredentials: true})
|
||||
this.http.get<User>(`${environment.apiUrl}/user/group/currentuser`, {withCredentials: true})
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe(
|
||||
{
|
||||
next: user => this.appState.authenticatedUser = user,
|
||||
next: user => this.appState.authenticateGroupUser(user),
|
||||
error: err => {
|
||||
if (err.status === 404 || err.status === 403) {
|
||||
console.warn('No default user is defined on this computer')
|
||||
|
@ -55,67 +52,74 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
login(id: number, password: string, success: () => void) {
|
||||
const loginForm = {id, password}
|
||||
globalLoadingWheel.show()
|
||||
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
|
||||
login(userId: number, password: string): Observable<any> {
|
||||
const subject = new Subject<void>()
|
||||
|
||||
this.http.post<any>(`${environment.apiUrl}/login`, {id: userId, password}, {
|
||||
withCredentials: true,
|
||||
observe: 'response' as 'body'
|
||||
})
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: (response: HttpResponse<any>) => {
|
||||
this.appState.authenticationExpiration = parseInt(response.headers.get('X-Authentication-Expiration'))
|
||||
this.appState.isAuthenticated = true
|
||||
this.setLoggedInUserFromApi()
|
||||
success()
|
||||
},
|
||||
error: err => {
|
||||
globalLoadingWheel.hide()
|
||||
if (err.status === 401 || err.status === 403) {
|
||||
this.alertService.pushError('Les identifiants entrés sont invalides')
|
||||
} else {
|
||||
this.errorService.handleError(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logout(success: () => void) {
|
||||
this.api.get<void>('/logout', true).pipe(
|
||||
}).pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.appState.resetAuthenticatedUser()
|
||||
this.checkAuthenticationStatus()
|
||||
success()
|
||||
},
|
||||
error: err => this.errorService.handleError(err)
|
||||
})
|
||||
).subscribe({
|
||||
next: (response: HttpResponse<void>) => {
|
||||
this.loginUser(response)
|
||||
|
||||
subject.next()
|
||||
subject.complete()
|
||||
},
|
||||
error: error => {
|
||||
if (error.status === 403) {
|
||||
this.alertService.pushError('Les identifiants entrés sont invalides')
|
||||
} else {
|
||||
this.errorService.handleError(error)
|
||||
}
|
||||
|
||||
subject.next()
|
||||
subject.complete()
|
||||
}
|
||||
})
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
private loginUser(response: HttpResponse<void>) {
|
||||
const authorization = response.headers.get("Authorization")
|
||||
const user = this.jwtService.parseUser(authorization)
|
||||
|
||||
this.appState.authenticateUser(user)
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
const subject = new Subject<void>()
|
||||
|
||||
this.api.get<void>('/logout').pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.logoutUser()
|
||||
|
||||
subject.next()
|
||||
subject.complete()
|
||||
},
|
||||
error: error => {
|
||||
this.errorService.handleError(error)
|
||||
|
||||
subject.next()
|
||||
subject.complete()
|
||||
}
|
||||
})
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
private logoutUser() {
|
||||
this.appState.resetAuthenticatedUser()
|
||||
this.checkAuthenticationStatus()
|
||||
}
|
||||
|
||||
hasPermission(permission: Permission): boolean {
|
||||
return this.appState.authenticatedUser && this.appState.authenticatedUser.permissions.indexOf(permission) >= 0
|
||||
}
|
||||
|
||||
private setLoggedInUserFromApi() {
|
||||
this.api.get<User>('/user/current', true)
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: user => {
|
||||
this.appState.authenticatedUser = user
|
||||
// At this point the loading wheel should be visible
|
||||
globalLoadingWheel.hide()
|
||||
},
|
||||
error: err => this.errorService.handleError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import {Injectable} from "@angular/core";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { User } from "../../shared/model/user";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class JwtService {
|
||||
parseUser(jwt: string): User {
|
||||
const decoded = jwtDecode(jwt) as any
|
||||
return JSON.parse(decoded.user)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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>
|
|
@ -1,6 +0,0 @@
|
|||
mat-card
|
||||
max-width: unset !important
|
||||
|
||||
td.units-wrapper p
|
||||
width: 3rem
|
||||
margin-bottom: 0
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<cre-entity-add
|
||||
title="Création d'une recette"
|
||||
backButtonLink="/color/list"
|
||||
[formFields]="formFields"
|
||||
(submit)="submit($event)">
|
||||
</cre-entity-add>
|
|
@ -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}`)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
.recipe-wrapper > div
|
||||
margin: 0 3rem 3rem
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
.recipe-content > div
|
||||
margin: 0 3rem 3rem
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<cre-mix-editor
|
||||
[recipeId]="recipeId"
|
||||
[materials]="materials"
|
||||
(save)="submit($event)">
|
||||
</cre-mix-editor>
|
|
@ -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}`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<cre-mix-editor
|
||||
[mixId]="mixId"
|
||||
[recipeId]="recipeId"
|
||||
[materials]="materials"
|
||||
(save)="submit($event)">
|
||||
</cre-mix-editor>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 { }
|
||||
|
|
|
@ -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 *ngIf="hasEditPermission" 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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
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"
|
||||
[configControl]="getConfigControl(keys.INSTANCE_LOGO_SET)" previewWidth="170px"
|
||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||
</cre-image-config>
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
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"
|
||||
[configControl]="getConfigControl(keys.INSTANCE_ICON_SET)" previewWidth="32px"
|
||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||
</cre-image-config>
|
||||
</cre-config-list>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
@ -65,10 +75,6 @@ abstract class _CreConfigBase extends SubscribingComponent {
|
|||
super(errorService, activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
get config(): Config {
|
||||
return this.configControl.config
|
||||
}
|
||||
|
@ -125,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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 *ngIf="hasEditPermission" 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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="!hasSimdut"
|
||||
[attr.title]="!hasSimdut ? 'Ce produit n\'a pas de fiche signalitique' : null"
|
||||
[disabled]="!material.hasSimdut"
|
||||
[attr.title]="!material.hasSimdut ? 'Ce produit n\'a pas de fiche signalitique' : null"
|
||||
(click)="openSimdut()">
|
||||
Voir la fiche signalitique
|
||||
</button>
|
||||
|
|
|
@ -119,10 +119,6 @@ export class EditComponent extends ErrorHandlingComponent {
|
|||
)
|
||||
}
|
||||
|
||||
get hasSimdut(): boolean {
|
||||
return this.material.simdutUrl != null
|
||||
}
|
||||
|
||||
openSimdut() {
|
||||
openSimdut(this.material)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -103,7 +98,7 @@
|
|||
|
||||
<ng-container matColumnDef="simdutIcon">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let material" [class.disabled]="materialHasSimdut(material)">
|
||||
<td mat-cell *matCellDef="let material" [class.disabled]="material.hasSimdut">
|
||||
<mat-icon
|
||||
svgIcon="text-box-remove"
|
||||
color="warn"
|
||||
|
@ -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]="!material.hasSimdut"
|
||||
(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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
@ -52,39 +64,26 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
|||
super.ngOnInit()
|
||||
|
||||
this.subscribe(
|
||||
this.materialService.allNotMixType,
|
||||
materials => {
|
||||
this.materials = materials
|
||||
this.dataSource = this.setupDataSource()
|
||||
},
|
||||
this.materialService.all,
|
||||
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,11 +91,7 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
|||
}
|
||||
|
||||
getQuantity(material: Material): number {
|
||||
return Math.round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units) * 100) / 100
|
||||
}
|
||||
|
||||
materialHasSimdut(material: Material): boolean {
|
||||
return material.simdutUrl != null
|
||||
return round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units), 2)
|
||||
}
|
||||
|
||||
openSimdut(material: Material) {
|
||||
|
@ -118,6 +113,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)
|
||||
}
|
||||
|
|
|
@ -17,10 +17,6 @@ export class MaterialService {
|
|||
return this.api.get<Material[]>('/material')
|
||||
}
|
||||
|
||||
get allNotMixType(): Observable<Material[]> {
|
||||
return this.api.get<Material[]>('/material/notmixtype')
|
||||
}
|
||||
|
||||
getAllForMixCreation(recipeId: number): Observable<Material[]> {
|
||||
return this.api.get<Material[]>(`/material/mix/create/${recipeId}`)
|
||||
}
|
||||
|
@ -33,14 +29,6 @@ export class MaterialService {
|
|||
return this.api.get<Material>(`/material/${id}`)
|
||||
}
|
||||
|
||||
hasSimdut(id: number): Observable<boolean> {
|
||||
return this.api.get<boolean>(`/material/${id}/simdut/exists`)
|
||||
}
|
||||
|
||||
getSimduts(): Observable<number[]> {
|
||||
return this.api.get<number[]>('/material/simdut')
|
||||
}
|
||||
|
||||
save(name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable<void> {
|
||||
const body = new FormData()
|
||||
body.append('name', name)
|
||||
|
|
|
@ -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-form-submit-button [form]="recipeForm.creForm" (submit)="recipeForm.submit()"></cre-form-submit-button>
|
||||
</cre-action-group>
|
||||
</cre-action-bar>
|
||||
|
||||
<recipe-form #recipeForm (submitForm)="submit($event)"></recipe-form>
|
|
@ -1,15 +1,17 @@
|
|||
<mat-card *ngIf="editionMode || imagesUrls">
|
||||
<mat-card *ngIf="editionMode || imagesIds">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Images</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content [class.no-action]="!editionMode">
|
||||
<div class="d-flex flex-row justify-content-around flex-wrap">
|
||||
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
|
||||
<p *ngIf="imagesIds.length <= 0" class="light-text text-center mb-0">Aucune image n'est associée à cette couleur</p>
|
||||
|
||||
<div *ngFor="let imageId of imagesIds" class="d-flex flex-column align-self-center m-3">
|
||||
<div class="image-wrapper">
|
||||
<img [src]="imageUrl" width="300px"/>
|
||||
<img [src]="getImageUrl(imageId)" width="300px"/>
|
||||
<div class="d-flex flex-row justify-content-end mt-2" [class.justify-content-between]="editionMode">
|
||||
<button mat-raised-button color="primary" (click)="openImage(imageUrl)">Afficher</button>
|
||||
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageUrl)">Retirer</button>
|
||||
<button mat-raised-button color="primary" (click)="openImage(imageId)">Afficher</button>
|
||||
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageId)">Retirer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -4,7 +4,8 @@ import {SubscribingComponent} from '../../../shared/components/subscribing.compo
|
|||
import {ActivatedRoute, Router} from '@angular/router'
|
||||
import {RecipeImageService} from '../../services/recipe-image.service'
|
||||
import {ErrorService} from '../../../shared/service/error.service'
|
||||
import {openJpg} from '../../../shared/utils/utils'
|
||||
import {getImageUrl, openJpg} from '../../../shared/utils/utils'
|
||||
import {RecipeService} from "../../services/recipe.service";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-images-editor',
|
||||
|
@ -15,10 +16,11 @@ export class ImagesEditorComponent extends SubscribingComponent {
|
|||
@Input() recipe: Recipe
|
||||
@Input() editionMode = false
|
||||
|
||||
imagesUrls: string[]
|
||||
imagesIds: string[] = []
|
||||
|
||||
constructor(
|
||||
private recipeImageService: RecipeImageService,
|
||||
private recipeService: RecipeService,
|
||||
errorService: ErrorService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
|
@ -29,19 +31,26 @@ export class ImagesEditorComponent extends SubscribingComponent {
|
|||
ngOnInit() {
|
||||
super.ngOnInit()
|
||||
|
||||
this.imagesUrls = this.recipe.imagesUrls
|
||||
this.subscribe(
|
||||
this.recipeService.getImagesIds(this.recipe.id),
|
||||
imagesIds => this.imagesIds = imagesIds ?? []
|
||||
)
|
||||
}
|
||||
|
||||
submit(event) {
|
||||
const image = event.target.files[0]
|
||||
this.subscribe(
|
||||
this.recipeImageService.save(image, this.recipe.id),
|
||||
r => this.imagesUrls = r.imagesUrls
|
||||
imageId => this.imagesIds.push(imageId)
|
||||
)
|
||||
}
|
||||
|
||||
openImage(url: string) {
|
||||
openJpg(url)
|
||||
getImageUrl(id: string): string {
|
||||
return getImageUrl(this.getImagePath(id))
|
||||
}
|
||||
|
||||
openImage(id: string) {
|
||||
openJpg(this.getImagePath(id))
|
||||
}
|
||||
|
||||
delete(url: string) {
|
||||
|
@ -52,6 +61,10 @@ export class ImagesEditorComponent extends SubscribingComponent {
|
|||
}
|
||||
|
||||
private removeUrl(url: string) {
|
||||
this.imagesUrls = this.imagesUrls.filter(u => u !== url)
|
||||
this.imagesIds = this.imagesIds.filter(u => u !== url)
|
||||
}
|
||||
|
||||
private getImagePath(id: string): string {
|
||||
return `recipes/${this.recipe.id}/${id}`
|
||||
}
|
||||
}
|
|
@ -126,7 +126,7 @@
|
|||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
[disabled]="!hasSimdut(getMixMaterialFromDto(mixMaterial).material)"
|
||||
[disabled]="!getMixMaterialFromDto(mixMaterial).material.hasSimdut"
|
||||
(click)="openSimdut(getMixMaterialFromDto(mixMaterial))">
|
||||
Fiche signalitique
|
||||
</button>
|
|
@ -1,5 +1,11 @@
|
|||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
|
||||
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsAsMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
|
||||
import {
|
||||
Mix,
|
||||
MixQuantity,
|
||||
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'
|
||||
|
@ -37,7 +43,7 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
mixColumns = this.COLUMNS
|
||||
units = UNIT_MILLILITER
|
||||
mixMaterials: MixMaterialDto[] = []
|
||||
hoveredMixMaterial: MixMaterial | null
|
||||
hoveredMixMaterial: MixQuantity | null
|
||||
|
||||
// BPac printer
|
||||
printer: PtouchPrinter | null
|
||||
|
@ -60,7 +66,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$,
|
||||
|
@ -68,11 +74,7 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
)
|
||||
}
|
||||
|
||||
hasSimdut(material: Material): boolean {
|
||||
return material.simdutUrl != null
|
||||
}
|
||||
|
||||
openSimdut(mixMaterial: MixMaterial) {
|
||||
openSimdut(mixMaterial: MixQuantity) {
|
||||
openSimdut(mixMaterial.material)
|
||||
}
|
||||
|
||||
|
@ -102,8 +104,8 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
})
|
||||
}
|
||||
|
||||
getMixMaterialFromDto(mixMaterialDto: MixMaterialDto): MixMaterial {
|
||||
return this.mix.mixMaterials.find(m => m.material.id === mixMaterialDto.materialId)
|
||||
getMixMaterialFromDto(mixMaterialDto: MixMaterialDto): MixQuantity {
|
||||
return this.mix.mixQuantities.find(m => m.material.id === mixMaterialDto.materialId)
|
||||
}
|
||||
|
||||
getMixMaterialQuantityRounded(mixMaterial: MixMaterialDto): number {
|
||||
|
@ -121,7 +123,7 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
return totalQuantity
|
||||
}
|
||||
|
||||
getCalculatedQuantityHtml(mixMaterial: MixMaterial, index: number): string {
|
||||
getCalculatedQuantityHtml(mixMaterial: MixQuantity, index: number): string {
|
||||
const totalQuantity = this.round(this.getTotalQuantity(index))
|
||||
const addedQuantity = this.round(this.calculateQuantity(index))
|
||||
return `<span class="mix-calculated-quantity">+${addedQuantity}</span> <span class="mix-calculated-quantity">(${totalQuantity})</span>`
|
||||
|
@ -137,7 +139,7 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
}
|
||||
|
||||
async print() {
|
||||
const base = this.mix.mixMaterials
|
||||
const base = this.mix.mixQuantities
|
||||
.map(ma => ma.material)
|
||||
.filter(m => m.materialType.name === 'Base')[0]
|
||||
if (!base) {
|
||||
|
@ -191,12 +193,14 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
materialId: quantity.materialId,
|
||||
quantity: this.calculateQuantity(index),
|
||||
isPercents: quantity.isPercents,
|
||||
position: quantity.position
|
||||
position: quantity.position,
|
||||
units: UNIT_MILLILITER,
|
||||
isMixType: false // TODO
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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"
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
<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>
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -115,7 +143,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
}
|
||||
|
||||
deductMix() {
|
||||
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === this.deductedMixId)[0].mixMaterials[0]
|
||||
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === this.deductedMixId)[0].mixQuantities[0]
|
||||
if (this.quantitiesChanges.has(this.deductedMixId) && this.quantitiesChanges.get(this.deductedMixId).has(firstMixMaterial.material.id)) {
|
||||
const originalQuantity = firstMixMaterial.quantity
|
||||
const currentQuantity = this.quantitiesChanges.get(this.deductedMixId).get(firstMixMaterial.material.id)
|
||||
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,28 @@
|
|||
<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-warn-button (click)="deleteConfirmBox.show()">Supprimer</cre-warn-button>
|
||||
<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>
|
||||
|
||||
<cre-confirm-box
|
||||
#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>
|
||||
</ng-container>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<cre-combo-box
|
||||
*ngIf="entries"
|
||||
[control]="control"
|
||||
[entries]="entries">
|
||||
</cre-combo-box>
|
|
@ -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]="allMaterialsValues"
|
||||
[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>
|
|
@ -0,0 +1,278 @@
|
|||
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 => {
|
||||
// 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(this.materialAsInputEntry)
|
||||
}
|
||||
|
||||
private materialAsInputEntry(material: Material): CreInputEntry {
|
||||
return new CreInputEntry(
|
||||
material.id,
|
||||
material.name,
|
||||
material.materialType.prefix ? `[${material.materialType.prefix}] ${material.name}` : material.name,
|
||||
{
|
||||
bold: material.isMixType
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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 = new Map<number, Material>();
|
||||
allMaterialsValues = [];
|
||||
|
||||
private _controls: ControlsByPosition[] = []
|
||||
private _destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private cdRef: ChangeDetectorRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.materials.subscribe({
|
||||
next: materials => {
|
||||
this.allMaterials.clear()
|
||||
this.allMaterialsValues = materials
|
||||
materials.forEach(m => this.allMaterials.set(m.id, m))
|
||||
|
||||
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, false)
|
||||
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.get(mixMaterial.materialId)?.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.size
|
||||
}
|
||||
|
||||
get updatedMixMaterials(): MixMaterialDto[] {
|
||||
const updatedMixMaterials: MixMaterialDto[] = []
|
||||
this.mixMaterials.forEach(mixMaterial => {
|
||||
const controls = this.getControlsByPosition(mixMaterial.position).controls
|
||||
const materialId = controls.materialId.value;
|
||||
const material = this.allMaterials.get(materialId)
|
||||
|
||||
updatedMixMaterials.push({
|
||||
...mixMaterial,
|
||||
materialId,
|
||||
quantity: controls.quantity.value,
|
||||
units: controls.units.value,
|
||||
isPercents: material.materialType.usePercentages,
|
||||
isMixType: material.isMixType
|
||||
})
|
||||
})
|
||||
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
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
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,
|
||||
protected 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.fetchMaterials(recipe.id)
|
||||
}
|
||||
|
||||
get recipe(): Recipe {
|
||||
return this._recipe
|
||||
}
|
||||
|
||||
protected abstract fetchMaterials(recipeId: number): Observable<Material[]>
|
||||
|
||||
abstract submit(dto: MixSaveDto)
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-mix-add',
|
||||
templateUrl: 'add.html'
|
||||
})
|
||||
export class MixAdd extends _BaseMixPage {
|
||||
protected fetchMaterials(recipeId: number): Observable<Material[]> {
|
||||
return this.materialService.getAllForMixCreation(recipeId)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private mixId: number
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit()
|
||||
|
||||
this.mixId = this.urlUtils.parseIntUrlParam('id')
|
||||
|
||||
this.fetchMix()
|
||||
}
|
||||
|
||||
private fetchMix() {
|
||||
this.subscribe(
|
||||
this.mixService.getById(this.mixId),
|
||||
mix => this.mix = mix
|
||||
)
|
||||
}
|
||||
|
||||
protected fetchMaterials(recipeId: number): Observable<Material[]> {
|
||||
return this.materialService.getAllForMixUpdate(this.mixId)
|
||||
}
|
||||
|
||||
submit(dto: MixSaveDto) {
|
||||
this.subscribeAndNavigate(
|
||||
this.mixService.updateDto({...dto, id: this.mix.id}),
|
||||
`/color/edit/${this.recipe.id}`
|
||||
)
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribeAndNavigate(
|
||||
this.mixService.delete(this.mixId),
|
||||
'/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.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,
|
||||
mixQuantities: this.mixMaterialsForm.updatedMixMaterials
|
||||
}
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return this.infoForm?.valid && this.mixMaterialsForm?.valid
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue