Merge pull request 'develop' (#5) from develop into master
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
Reviewed-on: #5
This commit is contained in:
commit
30f8c44a54
|
@ -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/*
|
.history/*
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
/.angular/cache
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
/coverage
|
/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
|
FROM alpine:latest AS build
|
||||||
|
WORKDIR /usr/src/
|
||||||
WORKDIR /usr/bin/cre/
|
|
||||||
|
|
||||||
ARG ARTIFACT_NAME=ColorRecipesExplorer-ng
|
|
||||||
COPY $ARTIFACT_NAME.zip .
|
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
RUN apk add --no-cache zip
|
RUN apk add --no-cache nodejs npm
|
||||||
|
|
||||||
RUN unzip $ARTIFACT_NAME.zip
|
RUN npm install -g typescript@4.0.7 && \
|
||||||
RUN rm $ARTIFACT_NAME.zip
|
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;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
35
angular.json
35
angular.json
|
@ -22,18 +22,27 @@
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": true,
|
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets",
|
"src/assets",
|
||||||
{ "glob": "mdi.svg", "input": "./node_modules/@mdi/angular-material", "output": "./assets"}
|
{
|
||||||
|
"glob": "mdi.svg",
|
||||||
|
"input": "./node_modules/@mdi/angular-material",
|
||||||
|
"output": "./assets"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||||
"src/custom-theme.scss",
|
"src/custom-theme.scss",
|
||||||
"src/styles.sass"
|
"src/styles.sass"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
@ -63,7 +72,8 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
@ -100,15 +110,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
"builder": "@angular-eslint/builder:lint",
|
||||||
"options": {
|
"options": {
|
||||||
"tsConfig": [
|
"lintFilePatterns": [
|
||||||
"tsconfig.app.json",
|
"src/**/*.ts",
|
||||||
"tsconfig.spec.json",
|
"src/**/*.html"
|
||||||
"e2e/tsconfig.json"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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"
|
version: "3.1"
|
||||||
|
|
||||||
services:
|
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
|
image: mysql
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: "pass"
|
MYSQL_ROOT_PASSWORD: "pass"
|
||||||
MYSQL_DATABASE: "cre"
|
MYSQL_DATABASE: "cre"
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- "3307: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
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
cre_data:
|
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 {
|
http {
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /usr/bin/cre/dist/color-recipes-explorer-frontend;
|
root /usr/bin/;
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
67
package.json
67
package.json
|
@ -3,53 +3,66 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"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",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"browser": {
|
||||||
|
"fs": false
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~11.2.10",
|
"@angular/animations": "~12.2.14",
|
||||||
"@angular/cdk": "^11.2.11",
|
"@angular/cdk": "^12.2.13",
|
||||||
"@angular/common": "~11.2.10",
|
"@angular/common": "~12.2.14",
|
||||||
"@angular/compiler": "~11.2.10",
|
"@angular/compiler": "~12.2.14",
|
||||||
"@angular/core": "~11.2.10",
|
"@angular/core": "~12.2.14",
|
||||||
"@angular/forms": "~11.2.10",
|
"@angular/forms": "~12.2.14",
|
||||||
"@angular/material": "^11.2.9",
|
"@angular/material": "^12.2.13",
|
||||||
"@angular/platform-browser": "~11.2.10",
|
"@angular/platform-browser": "~12.2.14",
|
||||||
"@angular/platform-browser-dynamic": "~11.2.10",
|
"@angular/platform-browser-dynamic": "~12.2.14",
|
||||||
"@angular/router": "~11.2.10",
|
"@angular/router": "~12.2.14",
|
||||||
"@mdi/angular-material": "^5.7.55",
|
"@js-joda/core": "^4.3.1",
|
||||||
|
"@mdi/angular-material": "^6.5.95",
|
||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
"copy-webpack-plugin": "^6.2.1",
|
"copy-webpack-plugin": "^10.0.0",
|
||||||
"js-joda": "^1.11.0",
|
"jwt-decode": "^3.1.2",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"ngx-material-file-input": "^2.1.1",
|
"ngx-material-file-input": "^2.1.1",
|
||||||
"rxjs": "~6.5.4",
|
"rxjs": "^7.4.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.3.1",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.1102.9",
|
"@angular-devkit/build-angular": "^12.2.13",
|
||||||
"@angular/cli": "^11.2.11",
|
"@angular-eslint/builder": "4.3.0",
|
||||||
"@angular/compiler-cli": "~11.2.10",
|
"@angular-eslint/eslint-plugin": "4.3.0",
|
||||||
"@angular/language-service": "~11.2.10",
|
"@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/jasmine": "~3.6.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"codelyzer": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "4.16.1",
|
||||||
"jasmine-core": "~3.6.0",
|
"@typescript-eslint/parser": "4.16.1",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"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": "~6.3.2",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "^10.4.0",
|
||||||
"tslint": "~6.1.0",
|
"typescript": "~4.3.5"
|
||||||
"typescript": "~4.0.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {CatalogComponent} from './pages/catalog/catalog.component'
|
||||||
import {AdministrationComponent} from './pages/administration/administration.component'
|
import {AdministrationComponent} from './pages/administration/administration.component'
|
||||||
import {MiscComponent} from './pages/others/misc.component'
|
import {MiscComponent} from './pages/others/misc.component'
|
||||||
import {CreConfigEditor} from './modules/configuration/config'
|
import {CreConfigEditor} from './modules/configuration/config-editor'
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [{
|
const routes: Routes = [{
|
||||||
path: 'color',
|
path: 'color',
|
||||||
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule)
|
loadChildren: () => import('./modules/recipes/recipes.module').then(m => m.RecipesModule)
|
||||||
}, {
|
}, {
|
||||||
path: 'account',
|
path: 'account',
|
||||||
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)
|
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {SubscribingComponent} from './modules/shared/components/subscribing.comp
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {ErrorService} from './modules/shared/service/error.service'
|
import {ErrorService} from './modules/shared/service/error.service'
|
||||||
import {ConfigService} from './modules/shared/service/config.service'
|
import {ConfigService} from './modules/shared/service/config.service'
|
||||||
import {Config} from './modules/shared/model/config.model'
|
|
||||||
import {environment} from '../environments/environment'
|
import {environment} from '../environments/environment'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -38,7 +37,7 @@ export class AppComponent extends SubscribingComponent {
|
||||||
online => this.isServerOnline = online
|
online => this.isServerOnline = online
|
||||||
)
|
)
|
||||||
|
|
||||||
this.favIcon.href = environment.apiUrl + "/file?path=images%2Ficon"
|
this.favIcon.href = environment.apiUrl + "/config/icon"
|
||||||
}
|
}
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core'
|
||||||
import {Routes, RouterModule} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router'
|
||||||
|
import {LogoutComponent} from './pages/logout/logout.component'
|
||||||
|
import {Login} from './accounts'
|
||||||
|
|
||||||
import {LoginComponent} from './pages/login/login.component';
|
const routes: Routes = [{
|
||||||
import {LogoutComponent} from "./pages/logout/logout.component";
|
path: 'login',
|
||||||
|
component: Login
|
||||||
const routes: Routes = [{path: 'login', component: LoginComponent}, {path: 'logout', component: LogoutComponent}, {path: '', redirectTo: 'login'}];
|
}, {
|
||||||
|
path: 'logout',
|
||||||
|
component: LogoutComponent
|
||||||
|
}, {
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'login'
|
||||||
|
}]
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
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 {AccountsRoutingModule} from './accounts-routing.module'
|
||||||
import {LoginComponent} from './pages/login/login.component';
|
import {LoginComponent} from './pages/login/login.component'
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from '../shared/shared.module'
|
||||||
import {LogoutComponent} from './pages/logout/logout.component';
|
import {LogoutComponent} from './pages/logout/logout.component'
|
||||||
import {CommonModule} from "@angular/common";
|
import {Login} from './accounts'
|
||||||
import {BrowserModule} from "@angular/platform-browser";
|
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||||
|
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [LoginComponent, LogoutComponent],
|
declarations: [
|
||||||
|
LoginComponent,
|
||||||
|
LogoutComponent,
|
||||||
|
Login
|
||||||
|
],
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
AccountsRoutingModule,
|
AccountsRoutingModule,
|
||||||
|
CreInputsModule,
|
||||||
|
CreButtonsModule,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AccountsModule {
|
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 {
|
ngOnInit(): void {
|
||||||
this.errorService.activeErrorHandler = this
|
this.errorService.activeErrorHandler = this
|
||||||
|
|
||||||
if (this.accountService.isLoggedIn()) {
|
if (this.appState.isAuthenticated) {
|
||||||
this.router.navigate(['/color'])
|
this.router.navigate(['/color'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +44,12 @@ export class LoginComponent extends ErrorHandlingComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
this.accountService.login(
|
this.subscribe(
|
||||||
this.idFormControl.value,
|
this.accountService.login(
|
||||||
this.passwordFormControl.value,
|
this.idFormControl.value,
|
||||||
() => this.router.navigate(['/color'])
|
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 {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({
|
@Component({
|
||||||
selector: 'cre-logout',
|
selector: 'cre-logout',
|
||||||
templateUrl: './logout.component.html',
|
templateUrl: './logout.component.html',
|
||||||
styleUrls: ['./logout.component.sass']
|
styleUrls: ['./logout.component.sass']
|
||||||
})
|
})
|
||||||
export class LogoutComponent implements OnInit {
|
export class LogoutComponent extends SubscribingComponent {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private router: Router
|
private appState: AppState,
|
||||||
|
errorService: ErrorService,
|
||||||
|
router: Router,
|
||||||
|
activatedRoute: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
|
super(errorService, activatedRoute, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.accountService.isLoggedIn()) {
|
if (!this.appState.isAuthenticated) {
|
||||||
this.router.navigate(['/account/login'])
|
this.urlUtils.navigateTo('/account/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accountService.logout(() => {
|
this.subscribeAndNavigate(
|
||||||
this.router.navigate(['/account/login'])
|
this.accountService.logout(),
|
||||||
})
|
'/account/login'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import {Injectable, OnDestroy} from '@angular/core'
|
import {Injectable, OnDestroy} from '@angular/core'
|
||||||
import {Subject} from 'rxjs'
|
import {Observable, Subject} from 'rxjs'
|
||||||
import {take, takeUntil} from 'rxjs/operators'
|
import {take, takeUntil} from 'rxjs/operators'
|
||||||
import {AppState} from '../../shared/app-state'
|
import {AppState} from '../../shared/app-state'
|
||||||
import {HttpClient, HttpResponse} from '@angular/common/http'
|
import {HttpClient, HttpResponse} from '@angular/common/http'
|
||||||
import {environment} from '../../../../environments/environment'
|
import {environment} from '../../../../environments/environment'
|
||||||
import {ApiService} from '../../shared/service/api.service'
|
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 {ErrorService} from '../../shared/service/error.service'
|
||||||
import {globalLoadingWheel} from '../../shared/components/loading-wheel/loading-wheel.component'
|
|
||||||
import {AlertService} from '../../shared/service/alert.service'
|
import {AlertService} from '../../shared/service/alert.service'
|
||||||
|
import {JwtService} from "./jwt.service";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -20,6 +20,7 @@ export class AccountService implements OnDestroy {
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private api: ApiService,
|
private api: ApiService,
|
||||||
private appState: AppState,
|
private appState: AppState,
|
||||||
|
private jwtService: JwtService,
|
||||||
private errorService: ErrorService,
|
private errorService: ErrorService,
|
||||||
private alertService: AlertService
|
private alertService: AlertService
|
||||||
) {
|
) {
|
||||||
|
@ -30,20 +31,16 @@ export class AccountService implements OnDestroy {
|
||||||
this.destroy$.complete()
|
this.destroy$.complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
|
||||||
return this.appState.isAuthenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAuthenticationStatus() {
|
checkAuthenticationStatus() {
|
||||||
if (!this.appState.authenticatedUser) {
|
if (!this.appState.isAuthenticated) {
|
||||||
// Try to get current default group user
|
// 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(
|
.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
).subscribe(
|
).subscribe(
|
||||||
{
|
{
|
||||||
next: user => this.appState.authenticatedUser = user,
|
next: user => this.appState.authenticateGroupUser(user),
|
||||||
error: err => {
|
error: err => {
|
||||||
if (err.status === 404 || err.status === 403) {
|
if (err.status === 404 || err.status === 403) {
|
||||||
console.warn('No default user is defined on this computer')
|
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) {
|
login(userId: number, password: string): Observable<any> {
|
||||||
const loginForm = {id, password}
|
const subject = new Subject<void>()
|
||||||
globalLoadingWheel.show()
|
|
||||||
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
|
this.http.post<any>(`${environment.apiUrl}/login`, {id: userId, password}, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
observe: 'response' as 'body'
|
observe: 'response' as 'body'
|
||||||
})
|
}).pipe(
|
||||||
.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(
|
|
||||||
take(1),
|
take(1),
|
||||||
takeUntil(this.destroy$)
|
takeUntil(this.destroy$)
|
||||||
)
|
).subscribe({
|
||||||
.subscribe({
|
next: (response: HttpResponse<void>) => {
|
||||||
next: () => {
|
this.loginUser(response)
|
||||||
this.appState.resetAuthenticatedUser()
|
|
||||||
this.checkAuthenticationStatus()
|
subject.next()
|
||||||
success()
|
subject.complete()
|
||||||
},
|
},
|
||||||
error: err => this.errorService.handleError(err)
|
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 {
|
hasPermission(permission: Permission): boolean {
|
||||||
return this.appState.authenticatedUser && this.appState.authenticatedUser.permissions.indexOf(permission) >= 0
|
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 { EditComponent } from './pages/edit/edit.component';
|
||||||
import {CompanyRoutingModule} from "./company-routing.module";
|
import {CompanyRoutingModule} from "./company-routing.module";
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
|
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
||||||
|
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
|
import {CreTablesModule} from '../shared/components/tables/tables.module'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +16,10 @@ import {SharedModule} from "../shared/shared.module";
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CompanyRoutingModule,
|
CompanyRoutingModule,
|
||||||
SharedModule
|
SharedModule,
|
||||||
|
CreActionBarModule,
|
||||||
|
CreButtonsModule,
|
||||||
|
CreTablesModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CompanyModule { }
|
export class CompanyModule { }
|
||||||
|
|
|
@ -1,7 +1,26 @@
|
||||||
<cre-entity-list
|
<cre-action-bar [reverse]="true">
|
||||||
[entities$]="companies$"
|
<cre-action-group>
|
||||||
[columns]="columns"
|
<cre-accent-button *ngIf="hasEditPermission" routerLink="/catalog/company/add">Ajouter</cre-accent-button>
|
||||||
[buttons]="buttons"
|
</cre-action-group>
|
||||||
addLink="/catalog/company/add"
|
</cre-action-bar>
|
||||||
addPermission="EDIT_COMPANIES">
|
|
||||||
</cre-entity-list>
|
<cre-warning-alert *ngIf="companiesEmpty">
|
||||||
|
<p>Il n'y a actuellement aucune bannière enregistrée dans le système.</p>
|
||||||
|
<p *ngIf="hasEditPermission">Vous pouvez en créer une <b><a routerLink="/catalog/company/add">ici</a></b>.</p>
|
||||||
|
</cre-warning-alert>
|
||||||
|
|
||||||
|
<cre-table *ngIf="!companiesEmpty" class="mx-auto" [data]="companies$ | async" [columns]="columns">
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||||
|
<td mat-cell *matCellDef="let company">{{company.name}}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="editButton">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell [class.disabled]="!hasEditPermission" *matCellDef="let company; let i = index">
|
||||||
|
<cre-accent-button [creInteractiveCell]="i" routerLink="/catalog/company/edit/{{company.id}}">
|
||||||
|
Modifier
|
||||||
|
</cre-accent-button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
</cre-table>
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {Permission} from '../../../shared/model/user'
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {ErrorService} from '../../../shared/service/error.service'
|
import {ErrorService} from '../../../shared/service/error.service'
|
||||||
import {AppState} from '../../../shared/app-state'
|
import {AppState} from '../../../shared/app-state'
|
||||||
|
import {tap} from 'rxjs/operators'
|
||||||
|
import {AccountService} from '../../../accounts/services/account.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-list',
|
selector: 'cre-list',
|
||||||
|
@ -12,18 +14,14 @@ import {AppState} from '../../../shared/app-state'
|
||||||
styleUrls: ['./list.component.sass']
|
styleUrls: ['./list.component.sass']
|
||||||
})
|
})
|
||||||
export class ListComponent extends ErrorHandlingComponent {
|
export class ListComponent extends ErrorHandlingComponent {
|
||||||
companies$ = this.companyService.all
|
companies$ = this.companyService.all.pipe(tap(companies => this.companiesEmpty = companies.length <= 0))
|
||||||
columns = [
|
companiesEmpty = false
|
||||||
{def: 'name', title: 'Nom', valueFn: c => c.name}
|
|
||||||
]
|
columns = ['name', 'editButton']
|
||||||
buttons = [{
|
|
||||||
text: 'Modifier',
|
|
||||||
linkFn: t => `/catalog/company/edit/${t.id}`,
|
|
||||||
permission: Permission.EDIT_COMPANIES
|
|
||||||
}]
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private companyService: CompanyService,
|
private companyService: CompanyService,
|
||||||
|
private accountService: AccountService,
|
||||||
private appState: AppState,
|
private appState: AppState,
|
||||||
errorService: ErrorService,
|
errorService: ErrorService,
|
||||||
router: Router,
|
router: Router,
|
||||||
|
@ -32,4 +30,8 @@ export class ListComponent extends ErrorHandlingComponent {
|
||||||
super(errorService, activatedRoute, router)
|
super(errorService, activatedRoute, router)
|
||||||
this.appState.title = 'Bannières'
|
this.appState.title = 'Bannières'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasEditPermission(): boolean {
|
||||||
|
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ApiService} from "../../shared/service/api.service";
|
import {ApiService} from '../../shared/service/api.service';
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from 'rxjs';
|
||||||
import {Company} from "../../shared/model/company.model";
|
import {Company} from '../../shared/model/company.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
|
@ -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 class="w-50 x-centered">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>
|
<mat-card-title>{{label}}</mat-card-title>
|
||||||
<ng-content select="cre-config-label"></ng-content>
|
|
||||||
</mat-card-title>
|
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content [class.no-action]="!hasActions">
|
<mat-card-content [class.no-action]="!hasActions">
|
||||||
<ng-content select="cre-config-list"></ng-content>
|
<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 {NgModule} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
CreConfig,
|
CreBoolConfig,
|
||||||
CreConfigLabel,
|
|
||||||
CreConfigEditor,
|
|
||||||
CreConfigSection,
|
|
||||||
CreImageConfig,
|
|
||||||
CreConfigList,
|
|
||||||
CreConfigActions,
|
CreConfigActions,
|
||||||
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig
|
CreConfigContainer,
|
||||||
|
CreConfigList,
|
||||||
|
CreConfigSection,
|
||||||
|
CreDateConfig,
|
||||||
|
CreImageConfig,
|
||||||
|
CrePeriodConfig,
|
||||||
|
CreSecureConfig,
|
||||||
|
CreTextConfig
|
||||||
} from './config'
|
} from './config'
|
||||||
import {SharedModule} from '../shared/shared.module'
|
import {SharedModule} from '../shared/shared.module'
|
||||||
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||||
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
||||||
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
|
import {CreConfigEditor} from './config-editor'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CreConfigLabel,
|
|
||||||
CreConfigTooltip,
|
|
||||||
CreConfigEditor,
|
|
||||||
CreConfig,
|
|
||||||
CreImageConfig,
|
|
||||||
CreConfigSection,
|
|
||||||
CreConfigList,
|
CreConfigList,
|
||||||
CreConfigActions,
|
CreConfigActions,
|
||||||
|
CreConfigSection,
|
||||||
|
CreConfigContainer,
|
||||||
|
CreTextConfig,
|
||||||
|
CreImageConfig,
|
||||||
CreBoolConfig,
|
CreBoolConfig,
|
||||||
CrePeriodConfig,
|
CrePeriodConfig,
|
||||||
CreDateConfig
|
CreDateConfig,
|
||||||
|
CreSecureConfig,
|
||||||
|
CreConfigEditor
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
@ -35,4 +38,5 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
CreButtonsModule
|
CreButtonsModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ConfigModule { }
|
export class ConfigModule {
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
mat-hint
|
mat-hint
|
||||||
font-size: .8em
|
font-size: .8em
|
||||||
|
|
||||||
cre-config
|
cre-config-container
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
cre-input.has-hint
|
.cre-config:not(.cre-editable-config)
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
|
|
||||||
mat-hint
|
mat-hint
|
||||||
|
|
|
@ -3,7 +3,6 @@ import {
|
||||||
Component,
|
Component,
|
||||||
ContentChild,
|
ContentChild,
|
||||||
Directive,
|
Directive,
|
||||||
ElementRef,
|
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
|
@ -11,46 +10,13 @@ import {
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {ConfigService} from '../shared/service/config.service'
|
import {ConfigService} from '../shared/service/config.service'
|
||||||
import {Config} from '../shared/model/config.model'
|
import {Config, ConfigControl} from '../shared/model/config.model'
|
||||||
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
|
import {SubscribingComponent} from '../shared/components/subscribing.component'
|
||||||
import {ErrorService} from '../shared/service/error.service'
|
import {ErrorService} from '../shared/service/error.service'
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
|
import {formatDate, formatDateTime, getConfiguredImageUrl, getFileUrl, readFile} from '../shared/utils/utils'
|
||||||
import {FormControl, Validators} from '@angular/forms'
|
import {AbstractControl} from '@angular/forms'
|
||||||
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: 'cre-config-list'
|
selector: 'cre-config-list'
|
||||||
|
@ -70,6 +36,8 @@ export class CreConfigActions {
|
||||||
templateUrl: 'config-section.html'
|
templateUrl: 'config-section.html'
|
||||||
})
|
})
|
||||||
export class CreConfigSection {
|
export class CreConfigSection {
|
||||||
|
@Input() label: string
|
||||||
|
|
||||||
@ContentChild(CreConfigActions) actions: CreConfigActions
|
@ContentChild(CreConfigActions) actions: CreConfigActions
|
||||||
|
|
||||||
get hasActions(): boolean {
|
get hasActions(): boolean {
|
||||||
|
@ -78,17 +46,25 @@ export class CreConfigSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-config',
|
selector: 'cre-config-container',
|
||||||
templateUrl: 'config.html',
|
templateUrl: 'config-container.html',
|
||||||
styleUrls: ['config.sass']
|
styleUrls: ['config.sass'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreConfig extends SubscribingComponent {
|
export class CreConfigContainer {
|
||||||
@Input() config: { key: string, control: FormControl }
|
@Input() configuration?: Config
|
||||||
|
@Input() tooltip: string
|
||||||
|
|
||||||
@ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel
|
get readOnly(): boolean {
|
||||||
@ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip
|
return !this.configuration?.editable ?? true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configuration: Config | null
|
@Directive()
|
||||||
|
abstract class _CreConfigBase extends SubscribingComponent {
|
||||||
|
@Input() configControl: ConfigControl
|
||||||
|
@Input() label: string
|
||||||
|
@Input() tooltip?: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
@ -99,41 +75,103 @@ export class CreConfig extends SubscribingComponent {
|
||||||
super(errorService, activatedRoute, router)
|
super(errorService, activatedRoute, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
get config(): Config {
|
||||||
super.ngOnInit()
|
return this.configControl.config
|
||||||
|
|
||||||
this.subscribe(
|
|
||||||
this.configService.get(this.config.key),
|
|
||||||
config => this.setConfig(config)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig(config: Config) {
|
get control(): AbstractControl {
|
||||||
this.configuration = config
|
return this.configControl.control
|
||||||
this.config.control.setValue(config.content)
|
|
||||||
if (!config.editable) {
|
|
||||||
this.config.control.disable()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastUpdated(): string {
|
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({
|
@Component({
|
||||||
selector: 'cre-image-config',
|
selector: 'cre-image-config',
|
||||||
templateUrl: 'image.html',
|
templateUrl: 'config-image.html',
|
||||||
styleUrls: ['config.sass'],
|
styleUrls: ['config.sass'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreImageConfig extends CreConfig {
|
export class CreImageConfig extends _CreConfigBase {
|
||||||
@Input() previewWidth: string | null
|
@Input() previewWidth: string | null
|
||||||
|
|
||||||
@Output() invalidFormat = new EventEmitter<void>()
|
@Output() invalidFormat = new EventEmitter<void>()
|
||||||
|
|
||||||
updatedImage: any | null
|
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(
|
constructor(
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
errorService: ErrorService,
|
errorService: ErrorService,
|
||||||
|
@ -143,113 +181,12 @@ export class CreImageConfig extends CreConfig {
|
||||||
super(configService, errorService, activatedRoute, router)
|
super(configService, errorService, activatedRoute, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateImage(file: File): any {
|
onOpen() {
|
||||||
readFile(file, (content) => this.updatedImage = content)
|
this.initialValue = this.control.value
|
||||||
|
this.dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
get configuredImageUrl(): string {
|
this.control.setValue(this.initialValue)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {SharedModule} from "../shared/shared.module";
|
||||||
import { AddComponent } from './pages/add/add.component';
|
import { AddComponent } from './pages/add/add.component';
|
||||||
import { EditComponent } from './pages/edit/edit.component';
|
import { EditComponent } from './pages/edit/edit.component';
|
||||||
|
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
||||||
|
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
|
import {CreTablesModule} from '../shared/components/tables/tables.module'
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -13,7 +16,10 @@ import { EditComponent } from './pages/edit/edit.component';
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MaterialTypeRoutingModule,
|
MaterialTypeRoutingModule,
|
||||||
SharedModule
|
SharedModule,
|
||||||
|
CreActionBarModule,
|
||||||
|
CreButtonsModule,
|
||||||
|
CreTablesModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class MaterialTypeModule { }
|
export class MaterialTypeModule { }
|
||||||
|
|
|
@ -1,7 +1,40 @@
|
||||||
<cre-entity-list
|
<cre-action-bar [reverse]="true">
|
||||||
[entities$]="materialTypes$"
|
<cre-action-group>
|
||||||
[columns]="columns"
|
<cre-accent-button *ngIf="hasEditPermission" routerLink="/catalog/materialtype/add">Ajouter</cre-accent-button>
|
||||||
[buttons]="buttons"
|
</cre-action-group>
|
||||||
addLink="/catalog/materialtype/add"
|
</cre-action-bar>
|
||||||
addPermission="EDIT_MATERIAL_TYPES">
|
|
||||||
</cre-entity-list>
|
<cre-warning-alert *ngIf="materialTypesEmpty">
|
||||||
|
<p>Il n'y a actuellement aucun type de produit enregistré dans le système.</p>
|
||||||
|
<p *ngIf="hasEditPermission">Vous pouvez en créer un <b><a routerLink="/catalog/materialtype/add">ici</a></b>.</p>
|
||||||
|
</cre-warning-alert>
|
||||||
|
|
||||||
|
<cre-table
|
||||||
|
*ngIf="!materialTypesEmpty"
|
||||||
|
class="mx-auto"
|
||||||
|
[data]="materialTypes$ | async"
|
||||||
|
[columns]="columns">
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||||
|
<td mat-cell *matCellDef="let materialType">{{materialType.name}}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="prefix">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Préfix</th>
|
||||||
|
<td mat-cell *matCellDef="let materialType">{{materialType.prefix}}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="usePercentages">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Utilise les pourcentages</th>
|
||||||
|
<td mat-cell *matCellDef="let materialType">{{materialType.usePercentages ? 'Oui' : 'Non'}}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="editButton">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell [class.disabled]="!hasEditPermission" *matCellDef="let materialType; let i = index">
|
||||||
|
<cre-accent-button [creInteractiveCell]="i" routerLink="/catalog/materialtype/edit/{{materialType.id}}">
|
||||||
|
Modifier
|
||||||
|
</cre-accent-button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
</cre-table>
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {Permission} from '../../../shared/model/user'
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {ErrorService} from '../../../shared/service/error.service'
|
import {ErrorService} from '../../../shared/service/error.service'
|
||||||
import {AppState} from '../../../shared/app-state'
|
import {AppState} from '../../../shared/app-state'
|
||||||
|
import {tap} from 'rxjs/operators'
|
||||||
|
import {AccountService} from '../../../accounts/services/account.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-list',
|
selector: 'cre-list',
|
||||||
|
@ -12,23 +14,16 @@ import {AppState} from '../../../shared/app-state'
|
||||||
styleUrls: ['./list.component.sass']
|
styleUrls: ['./list.component.sass']
|
||||||
})
|
})
|
||||||
export class ListComponent extends ErrorHandlingComponent {
|
export class ListComponent extends ErrorHandlingComponent {
|
||||||
materialTypes$ = this.materialTypeService.all
|
materialTypes$ = this.materialTypeService.all.pipe(
|
||||||
columns = [
|
tap(materialTypes => this.materialTypesEmpty = materialTypes.length <= 0)
|
||||||
{def: 'name', title: 'Nom', valueFn: t => t.name},
|
)
|
||||||
{def: 'prefix', title: 'Préfixe', valueFn: t => t.prefix},
|
materialTypesEmpty = false
|
||||||
{def: 'usePercentages', title: 'Utilise les pourcentages', valueFn: t => t.usePercentages ? 'Oui' : 'Non'}
|
|
||||||
]
|
columns = ['name', 'prefix', 'usePercentages', 'editButton']
|
||||||
buttons = [
|
|
||||||
{
|
|
||||||
text: 'Modifier',
|
|
||||||
linkFn: t => `/catalog/materialtype/edit/${t.id}`,
|
|
||||||
permission: Permission.EDIT_MATERIAL_TYPES,
|
|
||||||
disabledFn: t => t.systemType
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private materialTypeService: MaterialTypeService,
|
private materialTypeService: MaterialTypeService,
|
||||||
|
private accountService: AccountService,
|
||||||
private appState: AppState,
|
private appState: AppState,
|
||||||
errorService: ErrorService,
|
errorService: ErrorService,
|
||||||
router: Router,
|
router: Router,
|
||||||
|
@ -37,4 +32,8 @@ export class ListComponent extends ErrorHandlingComponent {
|
||||||
super(errorService, activatedRoute, router)
|
super(errorService, activatedRoute, router)
|
||||||
this.appState.title = 'Types de produit'
|
this.appState.title = 'Types de produit'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasEditPermission(): boolean {
|
||||||
|
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,13 @@ import {InventoryComponent} from './pages/inventory/inventory.component';
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
import {AddComponent} from './pages/add/add.component';
|
import {AddComponent} from './pages/add/add.component';
|
||||||
import {EditComponent} from './pages/edit/edit.component';
|
import {EditComponent} from './pages/edit/edit.component';
|
||||||
import {ColorsModule} from '../colors/colors.module'
|
import {RecipesModule} from '../recipes/recipes.module'
|
||||||
import {MatSortModule} from '@angular/material/sort'
|
import {MatSortModule} from '@angular/material/sort'
|
||||||
import {FormsModule} from '@angular/forms'
|
import {FormsModule} from '@angular/forms'
|
||||||
|
import {CreTablesModule} from '../shared/components/tables/tables.module'
|
||||||
|
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||||
|
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
|
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -17,9 +21,13 @@ import {FormsModule} from '@angular/forms'
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MaterialRoutingModule,
|
MaterialRoutingModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
ColorsModule,
|
RecipesModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
CreTablesModule,
|
||||||
|
CreInputsModule,
|
||||||
|
CreButtonsModule,
|
||||||
|
CreActionBarModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class MaterialModule {
|
export class MaterialModule {
|
||||||
|
|
|
@ -1,62 +1,59 @@
|
||||||
<div class="action-bar backward">
|
<cre-action-bar>
|
||||||
<!-- Left -->
|
<cre-action-group>
|
||||||
<div class="d-flex flex-row">
|
<cre-input
|
||||||
<mat-form-field class="mr-4">
|
class="mr-4"
|
||||||
<mat-label>Recherche par code...</mat-label>
|
label="Recherche par code..."
|
||||||
<input
|
[control]="materialNameFilterControl">
|
||||||
matInput
|
</cre-input>
|
||||||
type="text"
|
<cre-select
|
||||||
[(ngModel)]="materialNameFilter"
|
class="mr-4"
|
||||||
(keyup)="filterDataSource()"/>
|
label="Recherche par type de produit"
|
||||||
</mat-form-field>
|
[control]="materialTypeFilterControl"
|
||||||
<mat-form-field *ngIf="materialTypes$ | async as materialTypes">
|
[entries]="materialTypesEntries$">
|
||||||
<mat-label>Recherche par type de produit</mat-label>
|
</cre-select>
|
||||||
<mat-select
|
<cre-checkbox-input
|
||||||
[(value)]="materialTypeFilter"
|
label="Basse quantité"
|
||||||
(valueChange)="filterDataSource()">
|
[control]="hideLowQuantityControl">
|
||||||
<mat-option
|
</cre-checkbox-input>
|
||||||
*ngFor="let materialType of materialTypes"
|
</cre-action-group>
|
||||||
[value]="materialType.id">
|
<cre-action-group>
|
||||||
{{materialType.name}}
|
<cre-input
|
||||||
</mat-option>
|
class="mr-4"
|
||||||
</mat-select>
|
label="Quantité faible"
|
||||||
</mat-form-field>
|
type="number"
|
||||||
</div>
|
step="0.01"
|
||||||
|
[(value)]="lowQuantityThreshold">
|
||||||
<!-- Right -->
|
</cre-input>
|
||||||
<div class="ml-auto">
|
|
||||||
<mat-form-field class="mr-4">
|
|
||||||
<mat-label>Quantité faible</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
[(ngModel)]="lowQuantityThreshold"/>
|
|
||||||
</mat-form-field>
|
|
||||||
<cre-unit-selector [(unit)]="units"></cre-unit-selector>
|
<cre-unit-selector [(unit)]="units"></cre-unit-selector>
|
||||||
<button
|
<cre-accent-button
|
||||||
*ngIf="canEditMaterial"
|
*ngIf="canEditMaterial"
|
||||||
class="ml-3"
|
class="ml-3"
|
||||||
mat-raised-button
|
|
||||||
color="accent"
|
|
||||||
routerLink="/catalog/material/add">
|
routerLink="/catalog/material/add">
|
||||||
Ajouter
|
Ajouter
|
||||||
</button>
|
</cre-accent-button>
|
||||||
</div>
|
</cre-action-group>
|
||||||
</div>
|
</cre-action-bar>
|
||||||
|
|
||||||
<table
|
<cre-warning-alert *ngIf="!loading && materials.length === 0">
|
||||||
mat-table
|
<p>Il n'y a actuellement aucun produit enregistré dans le système.</p>
|
||||||
matSort
|
<p *ngIf="canEditMaterial">Vous pouvez en créer un <b><a routerLink="/catalog/material/add">ici</a></b>.
|
||||||
|
</p>
|
||||||
|
</cre-warning-alert>
|
||||||
|
|
||||||
|
<cre-table
|
||||||
|
*ngIf="materials.length > 0"
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
[dataSource]="dataSource">
|
[filterPredicate]="materialFilterPredicate"
|
||||||
|
[filter]="filter"
|
||||||
|
[data]="materials"
|
||||||
|
[columns]="columns">
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Code</th>
|
<th mat-header-cell *matHeaderCellDef>Code</th>
|
||||||
<td mat-cell *matCellDef="let material">{{material.name}}</td>
|
<td mat-cell *matCellDef="let material">{{material.name}}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="materialType">
|
<ng-container matColumnDef="materialType">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type de produit</th>
|
<th mat-header-cell *matHeaderCellDef>Type de produit</th>
|
||||||
<td mat-cell *matCellDef="let material">{{material.materialType.name}}</td>
|
<td mat-cell *matCellDef="let material">{{material.materialType.name}}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -68,9 +65,7 @@
|
||||||
<ng-container matColumnDef="addQuantity">
|
<ng-container matColumnDef="addQuantity">
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
<td mat-cell [class.disabled]="!canAddToInventory" *matCellDef="let material; let i = index">
|
<td mat-cell [class.disabled]="!canAddToInventory" *matCellDef="let material; let i = index">
|
||||||
<div
|
<div [creInteractiveCell]="i" class="input-group">
|
||||||
[hidden]="!((!hoveredMaterial && i === 0) || (hoveredMaterial === material) || (selectedMaterial && selectedMaterial === material))"
|
|
||||||
class="input-group">
|
|
||||||
<input
|
<input
|
||||||
#addQuantityInput
|
#addQuantityInput
|
||||||
class="form-control w-50"
|
class="form-control w-50"
|
||||||
|
@ -91,7 +86,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="lowQuantityIcon">
|
<ng-container matColumnDef="lowQuantityIcon">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
<td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)">
|
<td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)">
|
||||||
<mat-icon
|
<mat-icon
|
||||||
svgIcon="format-color-fill"
|
svgIcon="format-color-fill"
|
||||||
|
@ -115,37 +110,23 @@
|
||||||
<ng-container matColumnDef="editButton">
|
<ng-container matColumnDef="editButton">
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial">
|
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial">
|
||||||
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
|
<cre-accent-button
|
||||||
<button
|
[creInteractiveCell]="i"
|
||||||
mat-raised-button
|
routerLink="/catalog/material/edit/{{material.id}}">
|
||||||
color="accent"
|
Modifier
|
||||||
routerLink="/catalog/material/edit/{{material.id}}">
|
</cre-accent-button>
|
||||||
Modifier
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="openSimdutButton">
|
<ng-container matColumnDef="openSimdutButton">
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="canEditMaterial">
|
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="canEditMaterial">
|
||||||
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
|
<cre-accent-button
|
||||||
<button
|
[creInteractiveCell]="i"
|
||||||
mat-raised-button
|
[disabled]="!materialHasSimdut(material)"
|
||||||
color="accent"
|
(click)="openSimdut(material)">
|
||||||
[disabled]="!materialHasSimdut(material)"
|
Fiche signalitique
|
||||||
(click)="openSimdut(material)">
|
</cre-accent-button>
|
||||||
Fiche signalitique
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</cre-table>
|
||||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
|
||||||
<tr
|
|
||||||
mat-row
|
|
||||||
class="entity-row"
|
|
||||||
*matRowDef="let material; columns: columns"
|
|
||||||
(mouseover)="hoveredMaterial = material">
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
.input-group-append button
|
|
||||||
border-radius: 0 4px 4px 0
|
|
||||||
|
|
||||||
mat-select
|
mat-select
|
||||||
margin-top: 4px
|
margin-top: 4px
|
||||||
|
|
||||||
.form-control
|
.input-group
|
||||||
width: 6rem
|
cre-input
|
||||||
|
width: 6rem
|
||||||
|
|
||||||
|
.input-group-append button
|
||||||
|
border-radius: 0 4px 4px 0
|
||||||
|
|
|
@ -4,14 +4,17 @@ import {MaterialService} from '../../service/material.service'
|
||||||
import {Permission} from '../../../shared/model/user'
|
import {Permission} from '../../../shared/model/user'
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {ErrorService} from '../../../shared/service/error.service'
|
import {ErrorService} from '../../../shared/service/error.service'
|
||||||
import {Material, openSimdut} from '../../../shared/model/material.model'
|
import {Material, materialFilterFieldSeparator, materialMatchesFilter, openSimdut} from '../../../shared/model/material.model'
|
||||||
import {AccountService} from '../../../accounts/services/account.service'
|
import {AccountService} from '../../../accounts/services/account.service'
|
||||||
import {convertQuantity, UNIT_MILLILITER} from '../../../shared/units'
|
import {convertQuantity, UNIT_MILLILITER} from '../../../shared/units'
|
||||||
import {MatSort} from '@angular/material/sort'
|
import {MatSort} from '@angular/material/sort'
|
||||||
import {MatTableDataSource} from '@angular/material/table'
|
|
||||||
import {MaterialTypeService} from '../../../material-type/service/material-type.service'
|
import {MaterialTypeService} from '../../../material-type/service/material-type.service'
|
||||||
import {InventoryService} from '../../service/inventory.service'
|
import {InventoryService} from '../../service/inventory.service'
|
||||||
import {AppState} from '../../../shared/app-state'
|
import {AppState} from '../../../shared/app-state'
|
||||||
|
import {FormControl} from '@angular/forms'
|
||||||
|
import {map} from 'rxjs/operators'
|
||||||
|
import {CreInputEntry} from '../../../shared/components/inputs/inputs'
|
||||||
|
import {round} from '../../../shared/utils/utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-list',
|
selector: 'cre-list',
|
||||||
|
@ -21,9 +24,10 @@ import {AppState} from '../../../shared/app-state'
|
||||||
export class InventoryComponent extends ErrorHandlingComponent {
|
export class InventoryComponent extends ErrorHandlingComponent {
|
||||||
@ViewChild(MatSort) sort: MatSort
|
@ViewChild(MatSort) sort: MatSort
|
||||||
|
|
||||||
materials: Material[] | null
|
materials: Material[] | null = []
|
||||||
materialTypes$ = this.materialTypeService.all
|
materialTypesEntries$ = this.materialTypeService.all.pipe(map(materialTypes => {
|
||||||
dataSource: MatTableDataSource<Material>
|
return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))
|
||||||
|
}))
|
||||||
|
|
||||||
columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton']
|
columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton']
|
||||||
hoveredMaterial: Material | null
|
hoveredMaterial: Material | null
|
||||||
|
@ -31,8 +35,16 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
||||||
|
|
||||||
units = UNIT_MILLILITER
|
units = UNIT_MILLILITER
|
||||||
lowQuantityThreshold = 100 // TEMPORARY will be in the application settings
|
lowQuantityThreshold = 100 // TEMPORARY will be in the application settings
|
||||||
materialTypeFilter = 1
|
|
||||||
materialNameFilter = ''
|
materialFilterPredicate = materialMatchesFilter
|
||||||
|
|
||||||
|
private materialTypeFilter = 1
|
||||||
|
private materialNameFilter = ''
|
||||||
|
private hideLowQuantity = false
|
||||||
|
|
||||||
|
materialTypeFilterControl = new FormControl(this.materialTypeFilter)
|
||||||
|
materialNameFilterControl = new FormControl(this.materialNameFilter)
|
||||||
|
hideLowQuantityControl = new FormControl(this.hideLowQuantity)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private materialService: MaterialService,
|
private materialService: MaterialService,
|
||||||
|
@ -53,38 +65,25 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
||||||
|
|
||||||
this.subscribe(
|
this.subscribe(
|
||||||
this.materialService.allNotMixType,
|
this.materialService.allNotMixType,
|
||||||
materials => {
|
materials => this.materials = materials,
|
||||||
this.materials = materials
|
|
||||||
this.dataSource = this.setupDataSource()
|
|
||||||
},
|
|
||||||
true,
|
true,
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
setupDataSource(): MatTableDataSource<Material> {
|
this.subscribe(
|
||||||
this.dataSource = new MatTableDataSource<Material>(this.materials)
|
this.materialTypeFilterControl.valueChanges,
|
||||||
this.dataSource.sortingDataAccessor = (material, header) => {
|
filter => this.materialTypeFilter = filter
|
||||||
switch (header) {
|
)
|
||||||
case 'materialType':
|
|
||||||
return material[header].name
|
|
||||||
case 'lowQuantityIcon':
|
|
||||||
return this.isLowQuantity(material)
|
|
||||||
default:
|
|
||||||
return material[header]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.dataSource.filterPredicate = (material, filter) => {
|
|
||||||
return (!this.materialTypeFilter || this.materialTypeFilter === 1 || material.materialType.id === this.materialTypeFilter) &&
|
|
||||||
(!this.materialNameFilter || material.name.toLowerCase().includes(this.materialNameFilter.toLowerCase()))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dataSource.sort = this.sort
|
this.subscribe(
|
||||||
return this.dataSource
|
this.materialNameFilterControl.valueChanges,
|
||||||
}
|
filter => this.materialNameFilter = filter
|
||||||
|
)
|
||||||
|
|
||||||
filterDataSource() {
|
this.subscribe(
|
||||||
this.dataSource.filter = 'filter'
|
this.hideLowQuantityControl.valueChanges,
|
||||||
|
filter => this.hideLowQuantity = filter
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isLowQuantity(material: Material) {
|
isLowQuantity(material: Material) {
|
||||||
|
@ -92,7 +91,7 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuantity(material: Material): number {
|
getQuantity(material: Material): number {
|
||||||
return Math.round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units) * 100) / 100
|
return round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units), 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
materialHasSimdut(material: Material): boolean {
|
materialHasSimdut(material: Material): boolean {
|
||||||
|
@ -118,6 +117,10 @@ export class InventoryComponent extends ErrorHandlingComponent {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filter(): string {
|
||||||
|
return [this.materialTypeFilter, this.materialNameFilter, this.hideLowQuantity, this.lowQuantityThreshold].join(materialFilterFieldSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
get canEditMaterial(): boolean {
|
get canEditMaterial(): boolean {
|
||||||
return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
|
return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-header>
|
||||||
<mat-card-content [class.no-action]="!editionMode">
|
<mat-card-content [class.no-action]="!editionMode">
|
||||||
<div class="d-flex flex-row justify-content-around flex-wrap">
|
<div class="d-flex flex-row justify-content-around flex-wrap">
|
||||||
|
<p *ngIf="imagesUrls.length <= 0" class="light-text text-center mb-0">Aucune image n'est associée à cette couleur</p>
|
||||||
|
|
||||||
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
|
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
|
||||||
<div class="image-wrapper">
|
<div class="image-wrapper">
|
||||||
<img [src]="imageUrl" width="300px"/>
|
<img [src]="imageUrl" width="300px"/>
|
|
@ -1,4 +1,4 @@
|
||||||
@import '../../../../../custom-theme'
|
@import "~src/variables"
|
||||||
|
|
||||||
mat-expansion-panel
|
mat-expansion-panel
|
||||||
width: 48rem
|
width: 48rem
|
|
@ -1,5 +1,5 @@
|
||||||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
|
||||||
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsAsMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
|
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsToMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
|
||||||
import {Subject} from 'rxjs'
|
import {Subject} from 'rxjs'
|
||||||
import {SubscribingComponent} from '../../../shared/components/subscribing.component'
|
import {SubscribingComponent} from '../../../shared/components/subscribing.component'
|
||||||
import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units'
|
import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units'
|
||||||
|
@ -60,7 +60,7 @@ export class MixTableComponent extends SubscribingComponent {
|
||||||
this.mixColumns = this.COLUMNS_EDIT
|
this.mixColumns = this.COLUMNS_EDIT
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix)
|
this.mixMaterials = mixMaterialsToMixMaterialsDto(this.mix)
|
||||||
|
|
||||||
this.subscribe(
|
this.subscribe(
|
||||||
this.units$,
|
this.units$,
|
||||||
|
@ -191,12 +191,13 @@ export class MixTableComponent extends SubscribingComponent {
|
||||||
materialId: quantity.materialId,
|
materialId: quantity.materialId,
|
||||||
quantity: this.calculateQuantity(index),
|
quantity: this.calculateQuantity(index),
|
||||||
isPercents: quantity.isPercents,
|
isPercents: quantity.isPercents,
|
||||||
position: quantity.position
|
position: quantity.position,
|
||||||
|
units: UNIT_MILLILITER
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertQuantities(newUnit: string) {
|
private convertQuantities(newUnit: string) {
|
||||||
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, this.units, newUnit))
|
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, newUnit))
|
||||||
this.units = newUnit
|
this.units = newUnit
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
<mat-card-title>Mélanges</mat-card-title>
|
<mat-card-title>Mélanges</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content [class.no-action]="!editionMode">
|
<mat-card-content [class.no-action]="!editionMode">
|
||||||
|
<p *ngIf="recipe.mixes.length <= 0" class="light-text text-center">Il n'y a aucun mélange dans cette couleur</p>
|
||||||
|
|
||||||
<ng-container *ngFor="let mix of recipe.mixes; let i = index">
|
<ng-container *ngFor="let mix of recipe.mixes; let i = index">
|
||||||
<cre-mix-table
|
<cre-mix-table
|
||||||
[class.no-top-margin]="i == 0"
|
[class.no-top-margin]="i == 0"
|
|
@ -3,7 +3,7 @@
|
||||||
<mat-card-title>Étapes</mat-card-title>
|
<mat-card-title>Étapes</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content class="no-action">
|
<mat-card-content class="no-action">
|
||||||
<mat-list>
|
<mat-list *ngIf="steps.length > 0">
|
||||||
<mat-list-item *ngFor="let step of steps">
|
<mat-list-item *ngFor="let step of steps">
|
||||||
{{step.position}}.<span class="space"></span>{{step.message}}
|
{{step.position}}.<span class="space"></span>{{step.message}}
|
||||||
</mat-list-item>
|
</mat-list-item>
|
|
@ -1,6 +1,6 @@
|
||||||
<mat-form-field [class.short]="short">
|
<mat-form-field [class.short]="short">
|
||||||
<mat-label *ngIf="showLabel">Unités</mat-label>
|
<mat-label *ngIf="showLabel">Unités</mat-label>
|
||||||
<mat-select [value]="unit" (selectionChange)="unitChange.emit($event.value)">
|
<mat-select [value]="unit" (selectionChange)="onUnitChange($event.value)">
|
||||||
<ng-container *ngIf="!short">
|
<ng-container *ngIf="!short">
|
||||||
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
|
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
|
||||||
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
|
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
|
|
@ -1,17 +1,28 @@
|
||||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'
|
||||||
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
|
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
|
||||||
|
import {FormControl} from '@angular/forms'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-unit-selector',
|
selector: 'cre-unit-selector',
|
||||||
templateUrl: './unit-selector.component.html',
|
templateUrl: './unit-selector.component.html',
|
||||||
styleUrls: ['./unit-selector.component.sass']
|
styleUrls: ['./unit-selector.component.sass']
|
||||||
})
|
})
|
||||||
export class UnitSelectorComponent {
|
export class UnitSelectorComponent implements OnInit {
|
||||||
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
|
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
|
||||||
|
|
||||||
@Input() unit = UNIT_MILLILITER
|
@Input() unit = UNIT_MILLILITER
|
||||||
@Input() showLabel = true
|
@Input() showLabel = true
|
||||||
@Input() short = false
|
@Input() short = false
|
||||||
|
@Input() control: FormControl | null
|
||||||
|
|
||||||
@Output() unitChange = new EventEmitter<string>()
|
@Output() unitChange = new EventEmitter<string>()
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.control?.setValue(this.unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnitChange(newUnit: string) {
|
||||||
|
this.control?.setValue(newUnit)
|
||||||
|
this.unitChange.emit(newUnit)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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 {Component} from '@angular/core'
|
||||||
import {RecipeService} from '../../services/recipe.service'
|
import {RecipeService} from './services/recipe.service'
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
|
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
|
||||||
import {MixMaterialDto, Recipe, recipeMixCount, recipeNoteForGroupId, recipeStepCount} from '../../../shared/model/recipe.model'
|
import {
|
||||||
|
MixMaterialDto,
|
||||||
|
Recipe,
|
||||||
|
recipeMixCount,
|
||||||
|
recipeNoteForGroupId,
|
||||||
|
recipeStepCount
|
||||||
|
} from '../shared/model/recipe.model'
|
||||||
import {Observable, Subject} from 'rxjs'
|
import {Observable, Subject} from 'rxjs'
|
||||||
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service'
|
import {ErrorHandler, ErrorService} from '../shared/service/error.service'
|
||||||
import {AlertService} from '../../../shared/service/alert.service'
|
import {AlertService} from '../shared/service/alert.service'
|
||||||
import {GlobalAlertHandlerComponent} from '../../../shared/components/global-alert-handler/global-alert-handler.component'
|
import {GlobalAlertHandlerComponent} from '../shared/components/global-alert-handler/global-alert-handler.component'
|
||||||
import {InventoryService} from '../../../material/service/inventory.service'
|
import {InventoryService} from '../material/service/inventory.service'
|
||||||
import {ConfirmBoxComponent} from '../../../shared/components/confirm-box/confirm-box.component'
|
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
||||||
import {GroupService} from '../../../groups/services/group.service'
|
import {GroupService} from '../groups/services/group.service'
|
||||||
import {AppState} from '../../../shared/app-state'
|
import {AppState} from '../shared/app-state'
|
||||||
import {AccountService} from '../../../accounts/services/account.service'
|
import {AccountService} from '../accounts/services/account.service'
|
||||||
import {Permission} from '../../../shared/model/user'
|
import {Permission} from '../shared/model/user'
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
import {map} from 'rxjs/operators';
|
||||||
|
import {CreInputEntry} from '../shared/components/inputs/inputs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-explore',
|
selector: 'cre-recipe-explore',
|
||||||
templateUrl: './explore.component.html',
|
templateUrl: './explore.html',
|
||||||
styleUrls: ['./explore.component.sass']
|
styleUrls: ['./recipes.sass']
|
||||||
})
|
})
|
||||||
export class ExploreComponent extends ErrorHandlingComponent {
|
export class CreRecipeExplore extends ErrorHandlingComponent {
|
||||||
recipe: Recipe | null
|
|
||||||
groups$ = this.groupService.all
|
|
||||||
deductErrorBody = {}
|
deductErrorBody = {}
|
||||||
units$ = new Subject<string>()
|
units$ = new Subject<string>()
|
||||||
selectedGroupId: number | null
|
selectedGroupId: number | null
|
||||||
|
@ -33,6 +40,12 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
||||||
|
|
||||||
deductedMixId: number | null
|
deductedMixId: number | null
|
||||||
|
|
||||||
|
groupControl: FormControl
|
||||||
|
noteControl: FormControl
|
||||||
|
groupEntries$ = this.groupService.all.pipe(map(groups => {
|
||||||
|
return groups.map(group => new CreInputEntry(group.id, group.name))
|
||||||
|
}))
|
||||||
|
|
||||||
errorHandlers: ErrorHandler[] = [{
|
errorHandlers: ErrorHandler[] = [{
|
||||||
filter: error => error.type === 'notfound-recipe-id',
|
filter: error => error.type === 'notfound-recipe-id',
|
||||||
consumer: error => this.urlUtils.navigateTo('/color/list')
|
consumer: error => this.urlUtils.navigateTo('/color/list')
|
||||||
|
@ -42,6 +55,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
||||||
messageProducer: () => 'Certains produit ne sont pas en quantité suffisante dans l\'inventaire'
|
messageProducer: () => 'Certains produit ne sont pas en quantité suffisante dans l\'inventaire'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
private _recipe: Recipe | null
|
||||||
|
private _notePlaceholder = !this.canEditRecipesPublicData ? 'N/A' : ''
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private recipeService: RecipeService,
|
private recipeService: RecipeService,
|
||||||
private inventoryService: InventoryService,
|
private inventoryService: InventoryService,
|
||||||
|
@ -62,18 +78,30 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
||||||
|
|
||||||
this.selectedGroupId = this.loggedInUserGroupId
|
this.selectedGroupId = this.loggedInUserGroupId
|
||||||
|
|
||||||
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
this.fetchRecipe()
|
||||||
|
|
||||||
|
this.groupControl = new FormControl(this.selectedGroupId)
|
||||||
|
this.subscribe(
|
||||||
|
this.groupControl.valueChanges,
|
||||||
|
groupId => {
|
||||||
|
this.selectedGroupId = groupId
|
||||||
|
this.noteControl.setValue(this.selectedGroupNote, {emitEvent: false})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.noteControl = new FormControl({value: this._notePlaceholder, disabled: !this.canEditRecipesPublicData})
|
||||||
|
this.subscribe(
|
||||||
|
this.noteControl.valueChanges,
|
||||||
|
_ => this.hasModifications = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRecipe() {
|
||||||
|
const recipeId = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
||||||
this.subscribeEntityById(
|
this.subscribeEntityById(
|
||||||
this.recipeService,
|
this.recipeService,
|
||||||
id,
|
recipeId,
|
||||||
r => {
|
recipe => this.recipe = recipe
|
||||||
this.recipe = r
|
|
||||||
this.appState.title = r.name
|
|
||||||
|
|
||||||
if (recipeMixCount(this.recipe) <= 0 || recipeStepCount(this.recipe) <= 0) {
|
|
||||||
this.alertService.pushWarning('Cette recette n\'est pas complète')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,11 +156,24 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
||||||
subscribeDeductMix(observable: Observable<any>) {
|
subscribeDeductMix(observable: Observable<any>) {
|
||||||
this.subscribe(
|
this.subscribe(
|
||||||
observable,
|
observable,
|
||||||
() => this.alertService.pushSuccess('Les quantités quantités ont été déduites de l\'inventaire'),
|
() => this.alertService.pushSuccess('Les quantités ont été déduites de l\'inventaire'),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get recipe(): Recipe {
|
||||||
|
return this._recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
set recipe(recipe: Recipe) {
|
||||||
|
this._recipe = recipe
|
||||||
|
this.appState.title = recipe.name
|
||||||
|
|
||||||
|
if (recipeMixCount(recipe) <= 0 || recipeStepCount(recipe) <= 0) {
|
||||||
|
this.alertService.pushWarning('Cette recette n\'est pas complète')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get loggedInUserGroupId(): number {
|
get loggedInUserGroupId(): number {
|
||||||
return this.appState.authenticatedUser.group?.id
|
return this.appState.authenticatedUser.group?.id
|
||||||
}
|
}
|
||||||
|
@ -141,11 +182,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
||||||
if (!this.groupsNote.has(this.selectedGroupId)) {
|
if (!this.groupsNote.has(this.selectedGroupId)) {
|
||||||
this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId))
|
this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId))
|
||||||
}
|
}
|
||||||
return this.groupsNote.get(this.selectedGroupId)
|
return this.groupsNote.get(this.selectedGroupId) ?? this._notePlaceholder
|
||||||
}
|
|
||||||
|
|
||||||
set selectedGroupNote(value: string) {
|
|
||||||
this.groupsNote.set(this.selectedGroupId, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get canEditRecipesPublicData(): boolean {
|
get canEditRecipesPublicData(): boolean {
|
||||||
|
@ -160,7 +197,9 @@ export class ExploreComponent extends ErrorHandlingComponent {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.groupsNote.forEach((content, groupId) => {
|
this.groupsNote.forEach((content, groupId) => {
|
||||||
updatedNotes.set(groupId, content)
|
if (content) {
|
||||||
|
updatedNotes.set(groupId, content)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return updatedNotes
|
return updatedNotes
|
|
@ -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