develop #5
|
@ -0,0 +1,6 @@
|
|||
**/node_modules
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
package-lock.json
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
global-variables:
|
||||
release: &release ${DRONE_BRANCH##**/}
|
||||
environment: &environment
|
||||
CRE_REGISTRY_IMAGE: registry.fyloz.dev:5443/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
|
||||
|
||||
kind: pipeline
|
||||
name: default
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: set-docker-tags-latest
|
||||
image: *alpine-image
|
||||
environment:
|
||||
<<: *environment
|
||||
commands:
|
||||
- echo -n "latest" > .tags
|
||||
when:
|
||||
branch: develop
|
||||
|
||||
- name: set-docker-tags-release
|
||||
image: *alpine-image
|
||||
environment:
|
||||
<<: *environment
|
||||
commands:
|
||||
- echo -n "latest-release,$CRE_RELEASE" > .tags
|
||||
when:
|
||||
branch: release/**
|
||||
|
||||
- name: containerize
|
||||
image: plugins/docker
|
||||
environment:
|
||||
<<: *environment
|
||||
settings:
|
||||
repo: *docker-registry-repo
|
||||
when:
|
||||
branch:
|
||||
- develop
|
||||
- release/**
|
||||
|
||||
- name: deploy
|
||||
image: alpine:latest
|
||||
environment:
|
||||
<<: *environment
|
||||
CRE_REGISTRY_IMAGE: *docker-registry-repo
|
||||
DEPLOY_SERVER:
|
||||
from_secret: deploy_server
|
||||
DEPLOY_SERVER_USERNAME:
|
||||
from_secret: deploy_server_username
|
||||
DEPLOY_SERVER_SSH_PORT:
|
||||
from_secret: deploy_server_ssh_port
|
||||
DEPLOY_SERVER_SSH_KEY:
|
||||
from_secret: deploy_server_ssh_key
|
||||
DEPLOY_CONTAINER_NAME: cre_frontend
|
||||
commands:
|
||||
- apk update
|
||||
- apk add --no-cache openssh-client
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$DEPLOY_SERVER_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||
- chmod 700 ~/.ssh/id_rsa
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
- ssh-keyscan -p $DEPLOY_SERVER_SSH_PORT -H $DEPLOY_SERVER >> ~/.ssh/known_hosts
|
||||
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker stop $DEPLOY_CONTAINER_NAME || true && docker rm $DEPLOY_CONTAINER_NAME || true"
|
||||
- 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
|
|
@ -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
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
variables:
|
||||
CI_REGISTRY_IMAGE_NG: "$CI_REGISTRY_IMAGE:latest-ng"
|
||||
CI_REGISTRY_IMAGE_FRONTEND: "$CI_REGISTRY_IMAGE:latest"
|
||||
|
||||
before_script:
|
||||
- docker info
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
|
||||
stages:
|
||||
- build
|
||||
- package
|
||||
- deploy
|
||||
|
||||
.only-master:
|
||||
only:
|
||||
- master
|
||||
|
||||
build:
|
||||
stage: build
|
||||
extends: .only-master
|
||||
script:
|
||||
- docker pull $CI_REGISTRY_IMAGE_NG || true
|
||||
- docker build --cache-from $CI_REGISTRY_IMAGE_NG -f ng.Dockerfile -t $CI_REGISTRY_IMAGE_NG .
|
||||
- docker push $CI_REGISTRY_IMAGE_NG
|
||||
|
||||
package:
|
||||
stage: package
|
||||
needs: ['build']
|
||||
extends: .only-master
|
||||
variables:
|
||||
PACKAGE_CONTAINER_NAME: "cre_frontend_package"
|
||||
ARTIFACT_NAME: "ColorRecipesExplorer-frontend-$CI_PIPELINE_IID"
|
||||
script:
|
||||
- apk update
|
||||
- apk add --no-cache zip
|
||||
- mkdir dist
|
||||
- docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_NG ng build --configuration=$ANGULAR_CONFIGURATION --output-hashing=none --stats-json --source-map=false
|
||||
- docker cp $PACKAGE_CONTAINER_NAME:/usr/src/cre/dist/color-recipes-explorer-frontend/ dist/
|
||||
- zip -r $ARTIFACT_NAME.zip dist/
|
||||
- docker build -t $CI_REGISTRY_IMAGE_FRONTEND --build-arg ARTIFACT_NAME=$ARTIFACT_NAME .
|
||||
- docker push $CI_REGISTRY_IMAGE_FRONTEND
|
||||
after_script:
|
||||
- docker stop $PACKAGE_CONTAINER_NAME || true
|
||||
- docker rm $PACKAGE_CONTAINER_NAME || true
|
||||
artifacts:
|
||||
paths:
|
||||
- $ARTIFACT_NAME.zip
|
||||
expire_in: 1 week
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
needs: ['package']
|
||||
extends: .only-master
|
||||
variables:
|
||||
DEPLOYED_CONTAINER_NAME: "cre_frontend"
|
||||
before_script:
|
||||
- apk update
|
||||
- apk add --no-cache openssh-client
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||
- chmod 700 ~/.ssh/id_rsa
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
- ssh-keyscan -p $DEPLOYMENT_SERVER_SSH_PORT -H $DEPLOYMENT_SERVER >> ~/.ssh/known_hosts
|
||||
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
||||
script:
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker stop $DEPLOYED_CONTAINER_NAME || true && docker rm $DEPLOYED_CONTAINER_NAME || true"
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY && docker pull $CI_REGISTRY_IMAGE_FRONTEND"
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:80 --name=$DEPLOYED_CONTAINER_NAME $CI_REGISTRY_IMAGE_FRONTEND"
|
||||
|
||||
|
34
Dockerfile
34
Dockerfile
|
@ -1,17 +1,29 @@
|
|||
FROM nginx:mainline-alpine
|
||||
|
||||
WORKDIR /usr/bin/cre/
|
||||
|
||||
ARG ARTIFACT_NAME=ColorRecipesExplorer-ng
|
||||
COPY $ARTIFACT_NAME.zip .
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
FROM alpine:latest AS build
|
||||
WORKDIR /usr/src/
|
||||
|
||||
RUN apk update
|
||||
RUN apk add --no-cache zip
|
||||
RUN apk add --no-cache nodejs npm
|
||||
|
||||
RUN unzip $ARTIFACT_NAME.zip
|
||||
RUN rm $ARTIFACT_NAME.zip
|
||||
RUN npm install -g typescript@4.0.7 && \
|
||||
npm install -g @angular/cli@11.2.9 || true --fo
|
||||
|
||||
EXPOSE 80
|
||||
ENV NG_CLI_ANALYTICS=ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG ANGULAR_CONFIGURATION=production
|
||||
|
||||
RUN npm install --force
|
||||
RUN ng build --configuration=$ANGULAR_CONFIGURATION --stats-json --source-map=false
|
||||
|
||||
FROM nginx:mainline-alpine
|
||||
|
||||
WORKDIR /usr/bin/
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=build /usr/src/dist/color-recipes-explorer-frontend/ .
|
||||
|
||||
ARG CRE_PORT=80
|
||||
EXPOSE $CRE_PORT
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
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,28 +1,28 @@
|
|||
version: "3.1"
|
||||
|
||||
services:
|
||||
database:
|
||||
cre.backend:
|
||||
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
|
||||
environment:
|
||||
spring_profiles_active: "mysql,debug"
|
||||
cre_database_url: "mysql://database/cre"
|
||||
cre_database_username: "root"
|
||||
cre_database_password: "pass"
|
||||
CRE_ENABLE_DB_UPDATE: 1
|
||||
server_port: 9090
|
||||
ports:
|
||||
- "9090:9090"
|
||||
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:
|
||||
- 3306:3306
|
||||
backend:
|
||||
image: fyloz.dev:5443/color-recipes-explorer/backend:master
|
||||
environment:
|
||||
spring_profiles_active: "mysql,debug"
|
||||
cre_database_url: "mysql://database:3306/cre"
|
||||
cre_database_username: "root"
|
||||
cre_database_password: "pass"
|
||||
CRE_ENABLE_DB_UPDATE: 0
|
||||
server_port: 9090
|
||||
ports:
|
||||
- 9090:9090
|
||||
volumes:
|
||||
- cre_data:/usr/bin/cre/data
|
||||
- cre_config:/usr/bin/cre/config
|
||||
- "3307:3306"
|
||||
|
||||
volumes:
|
||||
cre_data:
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /usr/src/cre/
|
||||
|
||||
RUN apk update
|
||||
RUN apk add --no-cache nodejs
|
||||
RUN apk add --no-cache npm
|
||||
|
||||
RUN npm install -g typescript@4.0.7
|
||||
RUN npm install -g @angular/cli@11.2.9 || true
|
||||
|
||||
ENV NG_CLI_ANALYTICS=ci
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN npm install --force
|
||||
|
||||
COPY . .
|
|
@ -5,7 +5,7 @@ events { worker_connections 1024; }
|
|||
http {
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/bin/cre/dist/color-recipes-explorer-frontend;
|
||||
root /usr/bin/;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
location / {
|
||||
|
|
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
@import "assets/sass/modules/fonts";
|
||||
@import "custom-theme";
|
||||
@import "~material-design-icons/iconfont/material-icons.css";
|
||||
|
||||
// Spacing
|
||||
$spacer: 1rem;
|
||||
$spacers: (
|
||||
1: $spacer * 0.5,
|
||||
2: $spacer * 0.75,
|
||||
3: $spacer,
|
||||
4: $spacer * 1.5,
|
||||
5: $spacer * 2
|
||||
);
|
||||
|
||||
// Colors
|
||||
$color-primary: map-get($theme-primary, 500);
|
||||
$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;
|
|
@ -3,12 +3,11 @@ import {Routes, RouterModule} from '@angular/router'
|
|||
import {CatalogComponent} from './pages/catalog/catalog.component'
|
||||
import {AdministrationComponent} from './pages/administration/administration.component'
|
||||
import {MiscComponent} from './pages/others/misc.component'
|
||||
import {CreConfigEditor} from './modules/configuration/config'
|
||||
|
||||
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,18 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {Routes, RouterModule} from '@angular/router';
|
||||
import {NgModule} from '@angular/core'
|
||||
import {RouterModule, Routes} from '@angular/router'
|
||||
import {LogoutComponent} from './pages/logout/logout.component'
|
||||
import {Login} 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: LogoutComponent
|
||||
}, {
|
||||
path: '',
|
||||
redirectTo: 'login'
|
||||
}]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
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 {LoginComponent} from './pages/login/login.component'
|
||||
import {SharedModule} from '../shared/shared.module'
|
||||
import {LogoutComponent} from './pages/logout/logout.component'
|
||||
import {Login} from './accounts'
|
||||
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [LoginComponent, LogoutComponent],
|
||||
declarations: [
|
||||
LoginComponent,
|
||||
LogoutComponent,
|
||||
Login
|
||||
],
|
||||
imports: [
|
||||
SharedModule,
|
||||
AccountsRoutingModule,
|
||||
CreInputsModule,
|
||||
CreButtonsModule,
|
||||
]
|
||||
})
|
||||
export class AccountsModule {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import {Component, HostListener, ViewChild} from '@angular/core'
|
||||
import {FormControl, Validators} from '@angular/forms'
|
||||
import {ErrorHandlingComponent} 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 {take, takeUntil} from "rxjs/operators";
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -31,7 +31,7 @@ export class LoginComponent extends ErrorHandlingComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.errorService.activeErrorHandler = this
|
||||
|
||||
if (this.accountService.isLoggedIn()) {
|
||||
if (this.appState.isAuthenticated) {
|
||||
this.router.navigate(['/color'])
|
||||
}
|
||||
|
||||
|
@ -44,10 +44,12 @@ export class LoginComponent extends ErrorHandlingComponent implements OnInit {
|
|||
}
|
||||
|
||||
submit() {
|
||||
this.accountService.login(
|
||||
this.idFormControl.value,
|
||||
this.passwordFormControl.value,
|
||||
() => this.router.navigate(['/color'])
|
||||
this.subscribe(
|
||||
this.accountService.login(
|
||||
this.idFormControl.value,
|
||||
this.passwordFormControl.value
|
||||
),
|
||||
response => console.log(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,35 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Component} from '@angular/core';
|
||||
import {AccountService} from "../../services/account.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {AppState} from "../../../shared/app-state";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {ErrorService} from "../../../shared/service/error.service";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-logout',
|
||||
templateUrl: './logout.component.html',
|
||||
styleUrls: ['./logout.component.sass']
|
||||
})
|
||||
export class LogoutComponent implements OnInit {
|
||||
export class LogoutComponent extends SubscribingComponent {
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private router: Router
|
||||
private appState: AppState,
|
||||
errorService: ErrorService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(errorService, activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.accountService.isLoggedIn()) {
|
||||
this.router.navigate(['/account/login'])
|
||||
if (!this.appState.isAuthenticated) {
|
||||
this.urlUtils.navigateTo('/account/login')
|
||||
}
|
||||
|
||||
this.accountService.logout(() => {
|
||||
this.router.navigate(['/account/login'])
|
||||
})
|
||||
this.subscribeAndNavigate(
|
||||
this.accountService.logout(),
|
||||
'/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'
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<div *ngIf="configuration" [attr.title]="tooltip?.content" class="d-flex flex-row justify-content-between align-items-center">
|
||||
<cre-checkbox-input [label]="label.content" [control]="config.control"></cre-checkbox-input>
|
||||
<mat-hint>{{lastUpdated}}</mat-hint>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||
<cre-checkbox-input [label]="label" [control]="control"></cre-checkbox-input>
|
||||
<mat-hint>{{inputHint}}</mat-hint>
|
||||
</div>
|
||||
</cre-config-container>
|
|
@ -0,0 +1,6 @@
|
|||
<div
|
||||
class="cre-config"
|
||||
[class.cre-readonly-config]="readOnly"
|
||||
[attr.title]="tooltip">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||
<cre-input
|
||||
class="w-100"
|
||||
type="text"
|
||||
[label]="label"
|
||||
[hint]="inputHint"
|
||||
[control]="control"
|
||||
[icon]="inputIcon"
|
||||
[iconTitle]="inputIconTitle"
|
||||
iconColor="warning">
|
||||
</cre-input>
|
||||
</cre-config-container>
|
|
@ -0,0 +1,120 @@
|
|||
<form *ngIf="form" [formGroup]="form">
|
||||
<cre-action-bar>
|
||||
<cre-action-group>
|
||||
<cre-primary-button routerLink="/color">Retour</cre-primary-button>
|
||||
</cre-action-group>
|
||||
<cre-action-group>
|
||||
<cre-accent-button [disabled]="!(form.dirty && form.valid)" (click)="onSubmit()">Enregistrer</cre-accent-button>
|
||||
</cre-action-group>
|
||||
</cre-action-bar>
|
||||
|
||||
<div class="d-flex flex-column" style="gap: 1.5rem">
|
||||
<cre-config-section *ngIf="!emergencyMode" label="Apparence">
|
||||
<cre-config-list>
|
||||
<cre-image-config
|
||||
label="Logo"
|
||||
tooltip="Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
|
||||
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
|
||||
'Ctrl+F5')."
|
||||
[configControl]="getConfigControl(keys.INSTANCE_LOGO_SET)" previewWidth="170px"
|
||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||
</cre-image-config>
|
||||
|
||||
<cre-image-config
|
||||
label="Icône"
|
||||
tooltip="Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
|
||||
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
|
||||
'Ctrl+F5')."
|
||||
[configControl]="getConfigControl(keys.INSTANCE_ICON_SET)" previewWidth="32px"
|
||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||
</cre-image-config>
|
||||
</cre-config-list>
|
||||
</cre-config-section>
|
||||
|
||||
<cre-config-section *ngIf="!emergencyMode" label="Données">
|
||||
<cre-config-list class="pt-2">
|
||||
<cre-period-config
|
||||
label="Période d'expiration de l'approbation de l'échantillon des recettes"
|
||||
[configControl]="getConfigControl(keys.RECIPE_APPROBATION_EXPIRATION)">
|
||||
</cre-period-config>
|
||||
|
||||
<cre-period-config
|
||||
label="Période d'expiration des kits de retouches complets"
|
||||
tooltip="Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront
|
||||
supprimés automatiquement."
|
||||
[configControl]="getConfigControl(keys.TOUCH_UP_KIT_EXPIRATION)">
|
||||
</cre-period-config>
|
||||
|
||||
<cre-bool-config
|
||||
label="Activer le cache des PDFs générés"
|
||||
tooltip="Cette option permet de stocker les PDFs générés sur le disque, ce qui permet d'accélérer
|
||||
l'accès aux PDFs si la lecture des fichiers cachés sur le disque est plus rapide que la génération d'un
|
||||
nouveau PDF."
|
||||
[configControl]="getConfigControl(keys.TOUCH_UP_KIT_CACHE_PDF)">
|
||||
</cre-bool-config>
|
||||
</cre-config-list>
|
||||
</cre-config-section>
|
||||
|
||||
<cre-config-section label="Système">
|
||||
<cre-config-list>
|
||||
<cre-text-config
|
||||
*ngIf="!emergencyMode"
|
||||
label="URL de l'instance"
|
||||
tooltip="Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques."
|
||||
[configControl]="getConfigControl(keys.INSTANCE_URL)">
|
||||
</cre-text-config>
|
||||
|
||||
<cre-text-config
|
||||
label="URL de la base de données"
|
||||
[configControl]="getConfigControl(keys.DATABASE_URL)">
|
||||
</cre-text-config>
|
||||
|
||||
<cre-text-config
|
||||
label="Utilisateur de la base de données"
|
||||
[configControl]="getConfigControl(keys.DATABASE_USER)">
|
||||
</cre-text-config>
|
||||
|
||||
<cre-secure-config
|
||||
label="Mot de passe de la base de données"
|
||||
buttonLabel="Modifier le mot de passe de la base de données"
|
||||
[configControl]="getConfigControl(keys.DATABASE_PASSWORD)">
|
||||
</cre-secure-config>
|
||||
|
||||
<cre-text-config
|
||||
label="Version de la base de données"
|
||||
[configControl]="getConfigControl(keys.DATABASE_VERSION)">
|
||||
</cre-text-config>
|
||||
|
||||
<cre-text-config
|
||||
label="Version de Color Recipes Explorer"
|
||||
[configControl]="getConfigControl(keys.BACKEND_BUILD_VERSION)">
|
||||
</cre-text-config>
|
||||
|
||||
<cre-date-config
|
||||
label="Date de compilation de Color Recipes Explorer"
|
||||
[configControl]="getConfigControl(keys.BACKEND_BUILD_TIME)">
|
||||
</cre-date-config>
|
||||
|
||||
<cre-text-config
|
||||
label="Version de Java"
|
||||
[configControl]="getConfigControl(keys.JAVA_VERSION)">
|
||||
</cre-text-config>
|
||||
|
||||
<cre-text-config
|
||||
label="Système d'exploitation"
|
||||
[configControl]="getConfigControl(keys.OPERATING_SYSTEM)">
|
||||
</cre-text-config>
|
||||
</cre-config-list>
|
||||
<cre-config-actions>
|
||||
<cre-warn-button (click)="restartConfirmBox.show()">Redémarrer le serveur</cre-warn-button>
|
||||
</cre-config-actions>
|
||||
</cre-config-section>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
|
||||
<cre-confirm-box #restartConfirmBox
|
||||
message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
|
||||
(confirm)="restart()"></cre-confirm-box>
|
||||
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
|
||||
(confirm)="reload()"></cre-confirm-box>
|
|
@ -0,0 +1,98 @@
|
|||
import {Component, ViewChild} from '@angular/core'
|
||||
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
|
||||
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
||||
import {buildFormControl, Config, ConfigControl} from '../shared/model/config.model'
|
||||
import {FormBuilder, FormControl, FormGroup} from '@angular/forms'
|
||||
import {ConfigService} from '../shared/service/config.service'
|
||||
import {ErrorService} from '../shared/service/error.service'
|
||||
import {ActivatedRoute, Router} from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'cre-config-editor',
|
||||
templateUrl: 'config-editor.html'
|
||||
})
|
||||
export class CreConfigEditor extends ErrorHandlingComponent {
|
||||
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
|
||||
|
||||
keys = {
|
||||
INSTANCE_NAME: Config.INSTANCE_NAME,
|
||||
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,
|
||||
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
|
||||
DATABASE_VERSION: Config.DATABASE_VERSION,
|
||||
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
|
||||
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
|
||||
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
|
||||
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
|
||||
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
|
||||
JAVA_VERSION: Config.JAVA_VERSION,
|
||||
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
|
||||
}
|
||||
|
||||
configs = new Map<string, Config>()
|
||||
form: FormGroup | null
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
formBuilder: FormBuilder,
|
||||
errorService: ErrorService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
router: Router
|
||||
) {
|
||||
super(errorService, activatedRoute, router)
|
||||
|
||||
this.fetchConfigurations(formBuilder)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
getConfigControl(key: string): ConfigControl {
|
||||
return {
|
||||
config: this.configs.get(key),
|
||||
control: this.form.controls[key] as FormControl
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.subscribe(
|
||||
this.configService.setFromForm(this.form),
|
||||
() => this.reload()
|
||||
)
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.subscribe(
|
||||
this.configService.restart(),
|
||||
() => this.restartConfirmBox.show()
|
||||
)
|
||||
}
|
||||
|
||||
reload() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
get emergencyMode(): boolean {
|
||||
return this.configs.get(Config.EMERGENCY_MODE).content === 'true';
|
||||
}
|
||||
|
||||
private fetchConfigurations(formBuilder: FormBuilder) {
|
||||
this.subscribe(
|
||||
this.configService.all,
|
||||
configurations => this.buildForm(formBuilder, configurations)
|
||||
)
|
||||
}
|
||||
|
||||
private buildForm(formBuilder: FormBuilder, configurations: Config[]) {
|
||||
const group = {}
|
||||
configurations.forEach(config => {
|
||||
group[config.key] = buildFormControl(config)
|
||||
this.configs.set(config.key, config)
|
||||
})
|
||||
this.form = formBuilder.group(group)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<div class="cre-image-config-label">
|
||||
<p>
|
||||
{{label}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<cre-config-container
|
||||
[configuration]="config"
|
||||
[tooltip]="tooltip">
|
||||
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||
<cre-file-input
|
||||
class="w-100"
|
||||
accept="image/png,image/jpeg,image/x-icon,image/svg+xml"
|
||||
[control]="control"
|
||||
(selection)="updateImage($event)"
|
||||
(invalidFormat)="invalidFormat.emit()">
|
||||
</cre-file-input>
|
||||
<div class="image-wrapper d-flex flex-column justify-content-end">
|
||||
<div>
|
||||
<img
|
||||
[src]="updatedImage ? updatedImage : imageUrl"
|
||||
[attr.width]="previewWidth ? previewWidth : null"
|
||||
class="mat-elevation-z3"/>
|
||||
</div>
|
||||
<mat-hint>{{lastUpdated}}</mat-hint>
|
||||
</div>
|
||||
</div>
|
||||
</cre-config-container>
|
|
@ -0,0 +1,7 @@
|
|||
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||
<cre-period-input
|
||||
[control]="control"
|
||||
[label]="label"
|
||||
[hint]="inputHint">
|
||||
</cre-period-input>
|
||||
</cre-config-container>
|
|
@ -1,8 +1,6 @@
|
|||
<mat-card class="w-50 x-centered">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<ng-content select="cre-config-label"></ng-content>
|
||||
</mat-card-title>
|
||||
<mat-card-title>{{label}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content [class.no-action]="!hasActions">
|
||||
<ng-content select="cre-config-list"></ng-content>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||
<cre-primary-button
|
||||
class="w-100 mb-3"
|
||||
(click)="onOpen()">
|
||||
{{buttonLabel}}
|
||||
</cre-primary-button>
|
||||
|
||||
<cre-prompt-dialog
|
||||
[title]="label"
|
||||
(cancel)="onCancel()">
|
||||
<cre-dialog-body>
|
||||
<cre-input
|
||||
class="w-100"
|
||||
type="password"
|
||||
label="Nouvelle valeur"
|
||||
[hint]="inputHint"
|
||||
[control]="control"
|
||||
[icon]="inputIcon"
|
||||
[iconTitle]="inputIconTitle"
|
||||
iconColor="warning">
|
||||
</cre-input>
|
||||
</cre-dialog-body>
|
||||
</cre-prompt-dialog>
|
||||
</cre-config-container>
|
|
@ -0,0 +1,11 @@
|
|||
<cre-config-container [tooltip]="tooltip">
|
||||
<cre-input
|
||||
class="w-100"
|
||||
[control]="control"
|
||||
[label]="label"
|
||||
[hint]="inputHint"
|
||||
[icon]="inputIcon"
|
||||
[iconTitle]="inputIconTitle"
|
||||
iconColor="warning">
|
||||
</cre-input>
|
||||
</cre-config-container>
|
|
@ -1,12 +0,0 @@
|
|||
<div *ngIf="configuration" [attr.title]="tooltip?.content">
|
||||
<cre-input [class.has-hint]="configuration.editable"
|
||||
class="w-100"
|
||||
[type]="config.key === 'database.password' ? 'password' : 'text'"
|
||||
[label]="label.content"
|
||||
[hint]="configuration.editable ? lastUpdated : null"
|
||||
[control]="config.control"
|
||||
[icon]="configuration.requireRestart ? 'alert' : null"
|
||||
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
|
||||
iconColor="warning">
|
||||
</cre-input>
|
||||
</div>
|
|
@ -1,32 +1,35 @@
|
|||
import {NgModule} from '@angular/core'
|
||||
import {
|
||||
CreConfig,
|
||||
CreConfigLabel,
|
||||
CreConfigEditor,
|
||||
CreConfigSection,
|
||||
CreImageConfig,
|
||||
CreConfigList,
|
||||
CreBoolConfig,
|
||||
CreConfigActions,
|
||||
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig
|
||||
CreConfigContainer,
|
||||
CreConfigList,
|
||||
CreConfigSection,
|
||||
CreDateConfig,
|
||||
CreImageConfig,
|
||||
CrePeriodConfig,
|
||||
CreSecureConfig,
|
||||
CreTextConfig
|
||||
} from './config'
|
||||
import {SharedModule} from '../shared/shared.module'
|
||||
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
||||
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||
import {CreConfigEditor} from './config-editor'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CreConfigLabel,
|
||||
CreConfigTooltip,
|
||||
CreConfigEditor,
|
||||
CreConfig,
|
||||
CreImageConfig,
|
||||
CreConfigSection,
|
||||
CreConfigList,
|
||||
CreConfigActions,
|
||||
CreConfigSection,
|
||||
CreConfigContainer,
|
||||
CreTextConfig,
|
||||
CreImageConfig,
|
||||
CreBoolConfig,
|
||||
CrePeriodConfig,
|
||||
CreDateConfig
|
||||
CreDateConfig,
|
||||
CreSecureConfig,
|
||||
CreConfigEditor
|
||||
],
|
||||
imports: [
|
||||
SharedModule,
|
||||
|
@ -35,4 +38,5 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
|||
CreButtonsModule
|
||||
]
|
||||
})
|
||||
export class ConfigModule { }
|
||||
export class ConfigModule {
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
mat-hint
|
||||
font-size: .8em
|
||||
|
||||
cre-config
|
||||
cre-config-container
|
||||
display: block
|
||||
|
||||
cre-input.has-hint
|
||||
.cre-config:not(.cre-editable-config)
|
||||
margin-bottom: 1em
|
||||
|
||||
mat-hint
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
Component,
|
||||
ContentChild,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
|
@ -11,46 +10,13 @@ import {
|
|||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import {ConfigService} from '../shared/service/config.service'
|
||||
import {Config} from '../shared/model/config.model'
|
||||
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
|
||||
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 {FormControl, Validators} from '@angular/forms'
|
||||
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
||||
import {environment} from '../../../environments/environment'
|
||||
|
||||
@Directive({
|
||||
selector: 'cre-config-label'
|
||||
})
|
||||
export class CreConfigLabel implements AfterViewInit {
|
||||
content: string
|
||||
|
||||
constructor(
|
||||
private element: ElementRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.content = this.element.nativeElement.innerHTML
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: 'cre-config-tooltip'
|
||||
})
|
||||
export class CreConfigTooltip implements AfterViewInit {
|
||||
content: string
|
||||
|
||||
constructor(
|
||||
private element: ElementRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.content = this.element.nativeElement.innerHTML
|
||||
}
|
||||
}
|
||||
import {formatDate, formatDateTime, getConfiguredImageUrl, getFileUrl, readFile} from '../shared/utils/utils'
|
||||
import {AbstractControl} from '@angular/forms'
|
||||
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
|
||||
|
||||
@Directive({
|
||||
selector: 'cre-config-list'
|
||||
|
@ -70,6 +36,8 @@ export class CreConfigActions {
|
|||
templateUrl: 'config-section.html'
|
||||
})
|
||||
export class CreConfigSection {
|
||||
@Input() label: string
|
||||
|
||||
@ContentChild(CreConfigActions) actions: CreConfigActions
|
||||
|
||||
get hasActions(): boolean {
|
||||
|
@ -78,17 +46,25 @@ export class CreConfigSection {
|
|||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-config',
|
||||
templateUrl: 'config.html',
|
||||
styleUrls: ['config.sass']
|
||||
selector: 'cre-config-container',
|
||||
templateUrl: 'config-container.html',
|
||||
styleUrls: ['config.sass'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CreConfig extends SubscribingComponent {
|
||||
@Input() config: { key: string, control: FormControl }
|
||||
export class CreConfigContainer {
|
||||
@Input() configuration?: Config
|
||||
@Input() tooltip: string
|
||||
|
||||
@ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel
|
||||
@ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip
|
||||
get readOnly(): boolean {
|
||||
return !this.configuration?.editable ?? true
|
||||
}
|
||||
}
|
||||
|
||||
configuration: Config | null
|
||||
@Directive()
|
||||
abstract class _CreConfigBase extends SubscribingComponent {
|
||||
@Input() configControl: ConfigControl
|
||||
@Input() label: string
|
||||
@Input() tooltip?: string
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
|
@ -99,41 +75,103 @@ export class CreConfig extends SubscribingComponent {
|
|||
super(errorService, activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit()
|
||||
|
||||
this.subscribe(
|
||||
this.configService.get(this.config.key),
|
||||
config => this.setConfig(config)
|
||||
)
|
||||
get config(): Config {
|
||||
return this.configControl.config
|
||||
}
|
||||
|
||||
setConfig(config: Config) {
|
||||
this.configuration = config
|
||||
this.config.control.setValue(config.content)
|
||||
if (!config.editable) {
|
||||
this.config.control.disable()
|
||||
}
|
||||
get control(): AbstractControl {
|
||||
return this.configControl.control
|
||||
}
|
||||
|
||||
get lastUpdated(): string {
|
||||
return 'Dernière mise à jour: ' + formatDateTime(this.configuration.lastUpdated)
|
||||
return 'Dernière mise à jour: ' + formatDateTime(this.config.lastUpdated)
|
||||
}
|
||||
|
||||
get inputHint(): string {
|
||||
return this.config?.editable ? this.lastUpdated : null
|
||||
}
|
||||
}
|
||||
|
||||
@Directive()
|
||||
abstract class _CreTextConfigBase extends _CreConfigBase {
|
||||
private static readonly REQUIRE_RESTART_ICON = 'alert'
|
||||
private static readonly REQUIRE_RESTART_ICON_TITLE = 'Requiert un redémarrage'
|
||||
|
||||
get inputIcon(): string {
|
||||
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON : null
|
||||
}
|
||||
|
||||
get inputIconTitle(): string {
|
||||
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON_TITLE : null
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-text-config',
|
||||
templateUrl: 'config-text.html',
|
||||
styleUrls: ['config.sass']
|
||||
})
|
||||
export class CreTextConfig extends _CreTextConfigBase {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-image-config',
|
||||
templateUrl: 'image.html',
|
||||
templateUrl: 'config-image.html',
|
||||
styleUrls: ['config.sass'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CreImageConfig extends CreConfig {
|
||||
export class CreImageConfig extends _CreConfigBase {
|
||||
@Input() previewWidth: string | null
|
||||
|
||||
@Output() invalidFormat = new EventEmitter<void>()
|
||||
|
||||
updatedImage: any | null
|
||||
|
||||
updateImage(file: File): any {
|
||||
readFile(file, (content) => this.updatedImage = content)
|
||||
}
|
||||
|
||||
get imageUrl(): string {
|
||||
const path = this.config.key == Config.INSTANCE_ICON_SET ? 'icon' : 'logo'
|
||||
return getConfiguredImageUrl(path)
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-bool-config',
|
||||
templateUrl: 'config-bool.html'
|
||||
})
|
||||
export class CreBoolConfig extends _CreConfigBase {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-period-config',
|
||||
templateUrl: 'config-period.html'
|
||||
})
|
||||
export class CrePeriodConfig extends _CreConfigBase {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-date-config',
|
||||
templateUrl: 'config-date.html'
|
||||
})
|
||||
export class CreDateConfig extends _CreTextConfigBase implements AfterViewInit {
|
||||
ngAfterViewInit(): void {
|
||||
this.control.setValue(formatDate(this.config.content))
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-secure-config',
|
||||
templateUrl: 'config-secure.html'
|
||||
})
|
||||
export class CreSecureConfig extends _CreTextConfigBase {
|
||||
@ViewChild(CrePromptDialog) dialog: CrePromptDialog
|
||||
|
||||
@Input() buttonLabel: string
|
||||
|
||||
private initialValue: string | null
|
||||
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
errorService: ErrorService,
|
||||
|
@ -143,113 +181,12 @@ export class CreImageConfig extends CreConfig {
|
|||
super(configService, errorService, activatedRoute, router)
|
||||
}
|
||||
|
||||
updateImage(file: File): any {
|
||||
readFile(file, (content) => this.updatedImage = content)
|
||||
onOpen() {
|
||||
this.initialValue = this.control.value
|
||||
this.dialog.show()
|
||||
}
|
||||
|
||||
|
||||
get configuredImageUrl(): string {
|
||||
return getFileUrl(this.configuration.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-bool-config',
|
||||
templateUrl: 'bool.html'
|
||||
})
|
||||
export class CreBoolConfig extends CreConfig {
|
||||
setConfig(config: Config) {
|
||||
super.setConfig(config)
|
||||
this.config.control.setValue(config.content === 'true')
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-period-config',
|
||||
templateUrl: 'period.html'
|
||||
})
|
||||
export class CrePeriodConfig extends CreConfig {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-date-config',
|
||||
templateUrl: 'date.html'
|
||||
})
|
||||
export class CreDateConfig extends CreConfig {
|
||||
setConfig(config: Config) {
|
||||
super.setConfig(config);
|
||||
this.config.control.setValue(formatDate(config.content))
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cre-config-editor',
|
||||
templateUrl: 'editor.html'
|
||||
})
|
||||
export class CreConfigEditor extends ErrorHandlingComponent {
|
||||
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
|
||||
|
||||
keys = {
|
||||
INSTANCE_NAME: Config.INSTANCE_NAME,
|
||||
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
|
||||
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
|
||||
INSTANCE_URL: Config.INSTANCE_URL,
|
||||
DATABASE_URL: Config.DATABASE_URL,
|
||||
DATABASE_USER: Config.DATABASE_USER,
|
||||
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
|
||||
DATABASE_VERSION: Config.DATABASE_VERSION,
|
||||
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
|
||||
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
|
||||
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
|
||||
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
|
||||
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
|
||||
JAVA_VERSION: Config.JAVA_VERSION,
|
||||
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
|
||||
}
|
||||
controls = new Map<string, FormControl>()
|
||||
emergencyMode: string | null
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
errorService: ErrorService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
router: Router
|
||||
) {
|
||||
super(errorService, activatedRoute, router)
|
||||
|
||||
for (let key in this.keys) {
|
||||
this.controls[this.keys[key]] = new FormControl(null, Validators.required)
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.subscribe(
|
||||
this.configService.get(Config.EMERGENCY_MODE),
|
||||
config => {
|
||||
this.emergencyMode = config.content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getConfig(key: string) {
|
||||
return {key, control: this.controls[key]}
|
||||
}
|
||||
|
||||
save() {
|
||||
this.subscribe(
|
||||
this.configService.set(this.controls),
|
||||
() => this.reload()
|
||||
)
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.subscribe(
|
||||
this.configService.restart(),
|
||||
() => this.restartConfirmBox.show()
|
||||
)
|
||||
}
|
||||
|
||||
reload() {
|
||||
window.location.reload()
|
||||
onCancel() {
|
||||
this.control.setValue(this.initialValue)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<div *ngIf="configuration" [attr.title]="tooltip?.content">
|
||||
<cre-input [class.has-hint]="configuration.editable"
|
||||
class="w-100"
|
||||
type="text"
|
||||
[label]="label.content"
|
||||
[hint]="configuration.editable ? lastUpdated : null"
|
||||
[control]="config.control"
|
||||
[icon]="configuration.requireRestart ? 'alert' : null"
|
||||
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
|
||||
iconColor="warning">
|
||||
</cre-input>
|
||||
</div>
|
|
@ -1,120 +0,0 @@
|
|||
<cre-action-bar>
|
||||
<cre-action-group>
|
||||
<cre-primary-button routerLink="/color">Retour</cre-primary-button>
|
||||
</cre-action-group>
|
||||
<cre-action-group>
|
||||
<cre-accent-button (click)="save()">Enregistrer</cre-accent-button>
|
||||
</cre-action-group>
|
||||
</cre-action-bar>
|
||||
|
||||
<div *ngIf="emergencyMode" class="d-flex flex-column" style="gap: 1.5rem">
|
||||
<cre-config-section *ngIf="emergencyMode === 'false'">
|
||||
<cre-config-label>Apparence</cre-config-label>
|
||||
<cre-config-list>
|
||||
<!-- <cre-config [config]="getConfig(keys.INSTANCE_NAME)">-->
|
||||
<!-- <cre-config-label>Nom de l'instance</cre-config-label>-->
|
||||
<!-- <cre-config-tooltip>-->
|
||||
<!-- Affiché dans la barre de titre du navigateur ou en survolant l'onglet de la page dans le navigateur.-->
|
||||
<!-- </cre-config-tooltip>-->
|
||||
<!-- </cre-config>-->
|
||||
|
||||
<cre-image-config [config]="getConfig(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
|
||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||
<cre-config-label>Logo</cre-config-label>
|
||||
<cre-config-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').
|
||||
</cre-config-tooltip>
|
||||
</cre-image-config>
|
||||
|
||||
<cre-image-config [config]="getConfig(keys.INSTANCE_ICON_PATH)" previewWidth="32px"
|
||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||
<cre-config-label>Icône</cre-config-label>
|
||||
<cre-config-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').
|
||||
</cre-config-tooltip>
|
||||
</cre-image-config>
|
||||
</cre-config-list>
|
||||
</cre-config-section>
|
||||
|
||||
<cre-config-section>
|
||||
<cre-config-label>Données</cre-config-label>
|
||||
<cre-config-list class="pt-2">
|
||||
<cre-period-config [config]="getConfig(keys.RECIPE_APPROBATION_EXPIRATION)">
|
||||
<cre-config-label>Période d'expiration de l'approbation de l'échantillon des recettes</cre-config-label>
|
||||
</cre-period-config>
|
||||
|
||||
<cre-period-config [config]="getConfig(keys.TOUCH_UP_KIT_EXPIRATION)">
|
||||
<cre-config-label>Période d'expiration des kits de retouches complets</cre-config-label>
|
||||
<cre-config-tooltip>
|
||||
Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront supprimés automatiquement.
|
||||
</cre-config-tooltip>
|
||||
</cre-period-config>
|
||||
|
||||
<cre-bool-config [config]="getConfig(keys.TOUCH_UP_KIT_CACHE_PDF)">
|
||||
<cre-config-label>Activer le cache des PDFs générés</cre-config-label>
|
||||
<cre-config-tooltip>
|
||||
Cette option permet de stocker les PDFs générés sur le disque, ce qui permet d'accélérer
|
||||
l'accès aux PDFs si la lecture des fichiers cachés sur le disque est plus rapide que la génération d'un
|
||||
nouveau PDF.
|
||||
</cre-config-tooltip>
|
||||
</cre-bool-config>
|
||||
</cre-config-list>
|
||||
</cre-config-section>
|
||||
|
||||
<cre-config-section>
|
||||
<cre-config-label>Système</cre-config-label>
|
||||
<cre-config-list>
|
||||
<cre-config [config]="getConfig(keys.INSTANCE_URL)">
|
||||
<cre-config-label>URL de l'instance</cre-config-label>
|
||||
<cre-config-tooltip>
|
||||
Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques.
|
||||
</cre-config-tooltip>
|
||||
</cre-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.DATABASE_URL)">
|
||||
<cre-config-label>URL de la base de données</cre-config-label>
|
||||
</cre-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.DATABASE_USER)">
|
||||
<cre-config-label>Utilisateur de la base de données</cre-config-label>
|
||||
</cre-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.DATABASE_PASSWORD)">
|
||||
<cre-config-label>Mot de passe de la base de données</cre-config-label>
|
||||
</cre-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.DATABASE_VERSION)">
|
||||
<cre-config-label>Version de la base de données</cre-config-label>
|
||||
</cre-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.BACKEND_BUILD_VERSION)">
|
||||
<cre-config-label>Version de Color Recipes Explorer</cre-config-label>
|
||||
</cre-config>
|
||||
|
||||
<cre-date-config [config]="getConfig(keys.BACKEND_BUILD_TIME)">
|
||||
<cre-config-label>Date de compilation de Color Recipes Explorer</cre-config-label>
|
||||
</cre-date-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.JAVA_VERSION)">
|
||||
<cre-config-label>Version de Java</cre-config-label>
|
||||
</cre-config>
|
||||
|
||||
<cre-config [config]="getConfig(keys.OPERATING_SYSTEM)">
|
||||
<cre-config-label>Système d'exploitation</cre-config-label>
|
||||
</cre-config>
|
||||
</cre-config-list>
|
||||
<cre-config-actions>
|
||||
<cre-warn-button (click)="restartConfirmBox.show()">Redémarrer le serveur</cre-warn-button>
|
||||
</cre-config-actions>
|
||||
</cre-config-section>
|
||||
</div>
|
||||
|
||||
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
|
||||
<cre-confirm-box #restartConfirmBox message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
|
||||
(confirm)="restart()"></cre-confirm-box>
|
||||
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
|
||||
(confirm)="reload()"></cre-confirm-box>
|
|
@ -1,26 +0,0 @@
|
|||
<div class="cre-image-config-label">
|
||||
<p>
|
||||
<ng-content select="cre-config-label"></ng-content>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="configuration"
|
||||
class="d-flex flex-row justify-content-between align-items-center"
|
||||
[attr.title]="tooltip?.content">
|
||||
<cre-file-input
|
||||
class="w-100"
|
||||
accept="image/png,image/jpeg,image/x-icon,image/svg+xml"
|
||||
[control]="config.control"
|
||||
(selection)="updateImage($event)"
|
||||
(invalidFormat)="invalidFormat.emit()">
|
||||
</cre-file-input>
|
||||
<div class="image-wrapper d-flex flex-column justify-content-end">
|
||||
<div>
|
||||
<img
|
||||
[src]="updatedImage ? updatedImage : configuredImageUrl"
|
||||
[attr.width]="previewWidth ? previewWidth : null"
|
||||
class="mat-elevation-z3"/>
|
||||
</div>
|
||||
<mat-hint>{{lastUpdated}}</mat-hint>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
<div *ngIf="configuration" [attr.title]="tooltip?.content">
|
||||
<cre-period-input
|
||||
[control]="config.control"
|
||||
[label]="label.content"
|
||||
[hint]="lastUpdated">
|
||||
</cre-period-input>
|
||||
</div>
|
|
@ -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 {
|
||||
|
|
|
@ -1,62 +1,59 @@
|
|||
<div class="action-bar backward">
|
||||
<!-- Left -->
|
||||
<div class="d-flex flex-row">
|
||||
<mat-form-field class="mr-4">
|
||||
<mat-label>Recherche par code...</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="materialNameFilter"
|
||||
(keyup)="filterDataSource()"/>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="materialTypes$ | async as materialTypes">
|
||||
<mat-label>Recherche par type de produit</mat-label>
|
||||
<mat-select
|
||||
[(value)]="materialTypeFilter"
|
||||
(valueChange)="filterDataSource()">
|
||||
<mat-option
|
||||
*ngFor="let materialType of materialTypes"
|
||||
[value]="materialType.id">
|
||||
{{materialType.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div class="ml-auto">
|
||||
<mat-form-field class="mr-4">
|
||||
<mat-label>Quantité faible</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
step="0.01"
|
||||
[(ngModel)]="lowQuantityThreshold"/>
|
||||
</mat-form-field>
|
||||
<cre-action-bar>
|
||||
<cre-action-group>
|
||||
<cre-input
|
||||
class="mr-4"
|
||||
label="Recherche par code..."
|
||||
[control]="materialNameFilterControl">
|
||||
</cre-input>
|
||||
<cre-select
|
||||
class="mr-4"
|
||||
label="Recherche par type de produit"
|
||||
[control]="materialTypeFilterControl"
|
||||
[entries]="materialTypesEntries$">
|
||||
</cre-select>
|
||||
<cre-checkbox-input
|
||||
label="Basse quantité"
|
||||
[control]="hideLowQuantityControl">
|
||||
</cre-checkbox-input>
|
||||
</cre-action-group>
|
||||
<cre-action-group>
|
||||
<cre-input
|
||||
class="mr-4"
|
||||
label="Quantité faible"
|
||||
type="number"
|
||||
step="0.01"
|
||||
[(value)]="lowQuantityThreshold">
|
||||
</cre-input>
|
||||
<cre-unit-selector [(unit)]="units"></cre-unit-selector>
|
||||
<button
|
||||
<cre-accent-button
|
||||
*ngIf="canEditMaterial"
|
||||
class="ml-3"
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
routerLink="/catalog/material/add">
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</cre-accent-button>
|
||||
</cre-action-group>
|
||||
</cre-action-bar>
|
||||
|
||||
<table
|
||||
mat-table
|
||||
matSort
|
||||
<cre-warning-alert *ngIf="!loading && materials.length === 0">
|
||||
<p>Il n'y a actuellement aucun produit enregistré dans le système.</p>
|
||||
<p *ngIf="canEditMaterial">Vous pouvez en créer un <b><a routerLink="/catalog/material/add">ici</a></b>.
|
||||
</p>
|
||||
</cre-warning-alert>
|
||||
|
||||
<cre-table
|
||||
*ngIf="materials.length > 0"
|
||||
class="mx-auto"
|
||||
[dataSource]="dataSource">
|
||||
[filterPredicate]="materialFilterPredicate"
|
||||
[filter]="filter"
|
||||
[data]="materials"
|
||||
[columns]="columns">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Code</th>
|
||||
<th mat-header-cell *matHeaderCellDef>Code</th>
|
||||
<td mat-cell *matCellDef="let material">{{material.name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="materialType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type de produit</th>
|
||||
<th mat-header-cell *matHeaderCellDef>Type de produit</th>
|
||||
<td mat-cell *matCellDef="let material">{{material.materialType.name}}</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -68,9 +65,7 @@
|
|||
<ng-container matColumnDef="addQuantity">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canAddToInventory" *matCellDef="let material; let i = index">
|
||||
<div
|
||||
[hidden]="!((!hoveredMaterial && i === 0) || (hoveredMaterial === material) || (selectedMaterial && selectedMaterial === material))"
|
||||
class="input-group">
|
||||
<div [creInteractiveCell]="i" class="input-group">
|
||||
<input
|
||||
#addQuantityInput
|
||||
class="form-control w-50"
|
||||
|
@ -91,7 +86,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="lowQuantityIcon">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header></th>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)">
|
||||
<mat-icon
|
||||
svgIcon="format-color-fill"
|
||||
|
@ -115,37 +110,23 @@
|
|||
<ng-container matColumnDef="editButton">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial">
|
||||
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
routerLink="/catalog/material/edit/{{material.id}}">
|
||||
Modifier
|
||||
</button>
|
||||
</ng-container>
|
||||
<cre-accent-button
|
||||
[creInteractiveCell]="i"
|
||||
routerLink="/catalog/material/edit/{{material.id}}">
|
||||
Modifier
|
||||
</cre-accent-button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="openSimdutButton">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="canEditMaterial">
|
||||
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
[disabled]="!materialHasSimdut(material)"
|
||||
(click)="openSimdut(material)">
|
||||
Fiche signalitique
|
||||
</button>
|
||||
</ng-container>
|
||||
<cre-accent-button
|
||||
[creInteractiveCell]="i"
|
||||
[disabled]="!materialHasSimdut(material)"
|
||||
(click)="openSimdut(material)">
|
||||
Fiche signalitique
|
||||
</cre-accent-button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
class="entity-row"
|
||||
*matRowDef="let material; columns: columns"
|
||||
(mouseover)="hoveredMaterial = material">
|
||||
</tr>
|
||||
</table>
|
||||
</cre-table>
|
||||
|
|
|
@ -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,
|
||||
|
@ -53,38 +65,25 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
|||
|
||||
this.subscribe(
|
||||
this.materialService.allNotMixType,
|
||||
materials => {
|
||||
this.materials = materials
|
||||
this.dataSource = this.setupDataSource()
|
||||
},
|
||||
materials => this.materials = materials,
|
||||
true,
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
setupDataSource(): MatTableDataSource<Material> {
|
||||
this.dataSource = new MatTableDataSource<Material>(this.materials)
|
||||
this.dataSource.sortingDataAccessor = (material, header) => {
|
||||
switch (header) {
|
||||
case 'materialType':
|
||||
return material[header].name
|
||||
case 'lowQuantityIcon':
|
||||
return this.isLowQuantity(material)
|
||||
default:
|
||||
return material[header]
|
||||
}
|
||||
}
|
||||
this.dataSource.filterPredicate = (material, filter) => {
|
||||
return (!this.materialTypeFilter || this.materialTypeFilter === 1 || material.materialType.id === this.materialTypeFilter) &&
|
||||
(!this.materialNameFilter || material.name.toLowerCase().includes(this.materialNameFilter.toLowerCase()))
|
||||
}
|
||||
this.subscribe(
|
||||
this.materialTypeFilterControl.valueChanges,
|
||||
filter => this.materialTypeFilter = filter
|
||||
)
|
||||
|
||||
this.dataSource.sort = this.sort
|
||||
return this.dataSource
|
||||
}
|
||||
this.subscribe(
|
||||
this.materialNameFilterControl.valueChanges,
|
||||
filter => this.materialNameFilter = filter
|
||||
)
|
||||
|
||||
filterDataSource() {
|
||||
this.dataSource.filter = 'filter'
|
||||
this.subscribe(
|
||||
this.hideLowQuantityControl.valueChanges,
|
||||
filter => this.hideLowQuantity = filter
|
||||
)
|
||||
}
|
||||
|
||||
isLowQuantity(material: Material) {
|
||||
|
@ -92,7 +91,7 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
|||
}
|
||||
|
||||
getQuantity(material: Material): number {
|
||||
return Math.round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units) * 100) / 100
|
||||
return round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units), 2)
|
||||
}
|
||||
|
||||
materialHasSimdut(material: Material): boolean {
|
||||
|
@ -118,6 +117,10 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
|||
)
|
||||
}
|
||||
|
||||
get filter(): string {
|
||||
return [this.materialTypeFilter, this.materialNameFilter, this.hideLowQuantity, this.lowQuantityThreshold].join(materialFilterFieldSeparator)
|
||||
}
|
||||
|
||||
get canEditMaterial(): boolean {
|
||||
return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -4,6 +4,8 @@
|
|||
</mat-card-header>
|
||||
<mat-card-content [class.no-action]="!editionMode">
|
||||
<div class="d-flex flex-row justify-content-around flex-wrap">
|
||||
<p *ngIf="imagesUrls.length <= 0" class="light-text text-center mb-0">Aucune image n'est associée à cette couleur</p>
|
||||
|
||||
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
|
||||
<div class="image-wrapper">
|
||||
<img [src]="imageUrl" width="300px"/>
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../../../../custom-theme'
|
||||
@import "~src/variables"
|
||||
|
||||
mat-expansion-panel
|
||||
width: 48rem
|
|
@ -1,5 +1,5 @@
|
|||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
|
||||
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsAsMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
|
||||
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsToMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
|
||||
import {Subject} from 'rxjs'
|
||||
import {SubscribingComponent} from '../../../shared/components/subscribing.component'
|
||||
import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units'
|
||||
|
@ -60,7 +60,7 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
this.mixColumns = this.COLUMNS_EDIT
|
||||
}
|
||||
|
||||
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix)
|
||||
this.mixMaterials = mixMaterialsToMixMaterialsDto(this.mix)
|
||||
|
||||
this.subscribe(
|
||||
this.units$,
|
||||
|
@ -191,12 +191,13 @@ export class MixTableComponent extends SubscribingComponent {
|
|||
materialId: quantity.materialId,
|
||||
quantity: this.calculateQuantity(index),
|
||||
isPercents: quantity.isPercents,
|
||||
position: quantity.position
|
||||
position: quantity.position,
|
||||
units: UNIT_MILLILITER
|
||||
})
|
||||
}
|
||||
|
||||
private convertQuantities(newUnit: string) {
|
||||
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, this.units, newUnit))
|
||||
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, newUnit))
|
||||
this.units = newUnit
|
||||
}
|
||||
|
|
@ -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 *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,27 +1,34 @@
|
|||
import {Component} from '@angular/core'
|
||||
import {RecipeService} from '../../services/recipe.service'
|
||||
import {RecipeService} from './services/recipe.service'
|
||||
import {ActivatedRoute, Router} from '@angular/router'
|
||||
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
|
||||
import {MixMaterialDto, Recipe, recipeMixCount, recipeNoteForGroupId, recipeStepCount} from '../../../shared/model/recipe.model'
|
||||
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
|
||||
import {
|
||||
MixMaterialDto,
|
||||
Recipe,
|
||||
recipeMixCount,
|
||||
recipeNoteForGroupId,
|
||||
recipeStepCount
|
||||
} from '../shared/model/recipe.model'
|
||||
import {Observable, Subject} from 'rxjs'
|
||||
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service'
|
||||
import {AlertService} from '../../../shared/service/alert.service'
|
||||
import {GlobalAlertHandlerComponent} from '../../../shared/components/global-alert-handler/global-alert-handler.component'
|
||||
import {InventoryService} from '../../../material/service/inventory.service'
|
||||
import {ConfirmBoxComponent} from '../../../shared/components/confirm-box/confirm-box.component'
|
||||
import {GroupService} from '../../../groups/services/group.service'
|
||||
import {AppState} from '../../../shared/app-state'
|
||||
import {AccountService} from '../../../accounts/services/account.service'
|
||||
import {Permission} from '../../../shared/model/user'
|
||||
import {ErrorHandler, ErrorService} from '../shared/service/error.service'
|
||||
import {AlertService} from '../shared/service/alert.service'
|
||||
import {GlobalAlertHandlerComponent} from '../shared/components/global-alert-handler/global-alert-handler.component'
|
||||
import {InventoryService} from '../material/service/inventory.service'
|
||||
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
||||
import {GroupService} from '../groups/services/group.service'
|
||||
import {AppState} from '../shared/app-state'
|
||||
import {AccountService} from '../accounts/services/account.service'
|
||||
import {Permission} from '../shared/model/user'
|
||||
import {FormControl} from '@angular/forms';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {CreInputEntry} from '../shared/components/inputs/inputs';
|
||||
|
||||
@Component({
|
||||
selector: 'cre-explore',
|
||||
templateUrl: './explore.component.html',
|
||||
styleUrls: ['./explore.component.sass']
|
||||
selector: 'cre-recipe-explore',
|
||||
templateUrl: './explore.html',
|
||||
styleUrls: ['./recipes.sass']
|
||||
})
|
||||
export class ExploreComponent extends ErrorHandlingComponent {
|
||||
recipe: Recipe | null
|
||||
groups$ = this.groupService.all
|
||||
export class CreRecipeExplore extends ErrorHandlingComponent {
|
||||
deductErrorBody = {}
|
||||
units$ = new Subject<string>()
|
||||
selectedGroupId: number | null
|
||||
|
@ -33,6 +40,12 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
|
||||
deductedMixId: number | null
|
||||
|
||||
groupControl: FormControl
|
||||
noteControl: FormControl
|
||||
groupEntries$ = this.groupService.all.pipe(map(groups => {
|
||||
return groups.map(group => new CreInputEntry(group.id, group.name))
|
||||
}))
|
||||
|
||||
errorHandlers: ErrorHandler[] = [{
|
||||
filter: error => error.type === 'notfound-recipe-id',
|
||||
consumer: error => this.urlUtils.navigateTo('/color/list')
|
||||
|
@ -42,6 +55,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
messageProducer: () => 'Certains produit ne sont pas en quantité suffisante dans l\'inventaire'
|
||||
}]
|
||||
|
||||
private _recipe: Recipe | null
|
||||
private _notePlaceholder = !this.canEditRecipesPublicData ? 'N/A' : ''
|
||||
|
||||
constructor(
|
||||
private recipeService: RecipeService,
|
||||
private inventoryService: InventoryService,
|
||||
|
@ -62,18 +78,30 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
|
||||
this.selectedGroupId = this.loggedInUserGroupId
|
||||
|
||||
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
||||
this.fetchRecipe()
|
||||
|
||||
this.groupControl = new FormControl(this.selectedGroupId)
|
||||
this.subscribe(
|
||||
this.groupControl.valueChanges,
|
||||
groupId => {
|
||||
this.selectedGroupId = groupId
|
||||
this.noteControl.setValue(this.selectedGroupNote, {emitEvent: false})
|
||||
}
|
||||
)
|
||||
|
||||
this.noteControl = new FormControl({value: this._notePlaceholder, disabled: !this.canEditRecipesPublicData})
|
||||
this.subscribe(
|
||||
this.noteControl.valueChanges,
|
||||
_ => this.hasModifications = true
|
||||
)
|
||||
}
|
||||
|
||||
fetchRecipe() {
|
||||
const recipeId = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
||||
this.subscribeEntityById(
|
||||
this.recipeService,
|
||||
id,
|
||||
r => {
|
||||
this.recipe = r
|
||||
this.appState.title = r.name
|
||||
|
||||
if (recipeMixCount(this.recipe) <= 0 || recipeStepCount(this.recipe) <= 0) {
|
||||
this.alertService.pushWarning('Cette recette n\'est pas complète')
|
||||
}
|
||||
}
|
||||
recipeId,
|
||||
recipe => this.recipe = recipe
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -128,11 +156,24 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
subscribeDeductMix(observable: Observable<any>) {
|
||||
this.subscribe(
|
||||
observable,
|
||||
() => this.alertService.pushSuccess('Les quantités quantités ont été déduites de l\'inventaire'),
|
||||
() => this.alertService.pushSuccess('Les quantités ont été déduites de l\'inventaire'),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
get recipe(): Recipe {
|
||||
return this._recipe
|
||||
}
|
||||
|
||||
set recipe(recipe: Recipe) {
|
||||
this._recipe = recipe
|
||||
this.appState.title = recipe.name
|
||||
|
||||
if (recipeMixCount(recipe) <= 0 || recipeStepCount(recipe) <= 0) {
|
||||
this.alertService.pushWarning('Cette recette n\'est pas complète')
|
||||
}
|
||||
}
|
||||
|
||||
get loggedInUserGroupId(): number {
|
||||
return this.appState.authenticatedUser.group?.id
|
||||
}
|
||||
|
@ -141,11 +182,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
if (!this.groupsNote.has(this.selectedGroupId)) {
|
||||
this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId))
|
||||
}
|
||||
return this.groupsNote.get(this.selectedGroupId)
|
||||
}
|
||||
|
||||
set selectedGroupNote(value: string) {
|
||||
this.groupsNote.set(this.selectedGroupId, value)
|
||||
return this.groupsNote.get(this.selectedGroupId) ?? this._notePlaceholder
|
||||
}
|
||||
|
||||
get canEditRecipesPublicData(): boolean {
|
||||
|
@ -160,7 +197,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
|||
})
|
||||
|
||||
this.groupsNote.forEach((content, groupId) => {
|
||||
updatedNotes.set(groupId, content)
|
||||
if (content) {
|
||||
updatedNotes.set(groupId, content)
|
||||
}
|
||||
})
|
||||
|
||||
return updatedNotes
|
|
@ -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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue