Merge pull request 'develop' (#5) from develop into master
continuous-integration/drone/push Build is failing Details

Reviewed-on: #5
This commit is contained in:
William Nolin 2021-12-15 00:25:09 -05:00
commit 30f8c44a54
178 changed files with 3431 additions and 2407 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
**/node_modules
.gitignore
.dockerignore
Dockerfile
docker-compose.yml
package-lock.json

79
.drone.yml Normal file
View File

@ -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

93
.eslintrc.json Normal file
View File

@ -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": {}
}
]
}

1
.gitignore vendored
View File

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

View File

@ -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"

View File

@ -1,17 +1,29 @@
FROM nginx:mainline-alpine
WORKDIR /usr/bin/cre/
ARG ARTIFACT_NAME=ColorRecipesExplorer-ng
COPY $ARTIFACT_NAME.zip .
COPY nginx.conf /etc/nginx/nginx.conf
FROM alpine:latest AS build
WORKDIR /usr/src/
RUN apk update
RUN apk add --no-cache zip
RUN apk add --no-cache nodejs npm
RUN unzip $ARTIFACT_NAME.zip
RUN rm $ARTIFACT_NAME.zip
RUN npm install -g typescript@4.0.7 && \
npm install -g @angular/cli@11.2.9 || true --fo
EXPOSE 80
ENV NG_CLI_ANALYTICS=ci
COPY . .
ARG ANGULAR_CONFIGURATION=production
RUN npm install --force
RUN ng build --configuration=$ANGULAR_CONFIGURATION --stats-json --source-map=false
FROM nginx:mainline-alpine
WORKDIR /usr/bin/
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /usr/src/dist/color-recipes-explorer-frontend/ .
ARG CRE_PORT=80
EXPOSE $CRE_PORT
CMD ["nginx", "-g", "daemon off;"]

View File

@ -22,18 +22,27 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets",
{ "glob": "mdi.svg", "input": "./node_modules/@mdi/angular-material", "output": "./assets"}
{
"glob": "mdi.svg",
"input": "./node_modules/@mdi/angular-material",
"output": "./assets"
}
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/custom-theme.scss",
"src/styles.sass"
],
"scripts": []
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
@ -63,7 +72,8 @@
}
]
}
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@ -100,15 +110,11 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
},
@ -127,5 +133,8 @@
}
}
},
"defaultProject": "color-recipes-explorer-frontend"
"defaultProject": "color-recipes-explorer-frontend",
"cli": {
"defaultCollection": "@angular-eslint/schematics"
}
}

View File

@ -1,28 +1,28 @@
version: "3.1"
services:
database:
cre.backend:
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 1
server_port: 9090
ports:
- "9090:9090"
volumes:
- cre_data:/usr/bin/cre/data
- cre_config:/usr/bin/cre/config
cre.database:
image: mysql
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- 3306:3306
backend:
image: fyloz.dev:5443/color-recipes-explorer/backend:master
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database:3306/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 0
server_port: 9090
ports:
- 9090:9090
volumes:
- cre_data:/usr/bin/cre/data
- cre_config:/usr/bin/cre/config
- "3307:3306"
volumes:
cre_data:

View File

@ -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 . .

View File

@ -5,7 +5,7 @@ events { worker_connections 1024; }
http {
server {
listen 80;
root /usr/bin/cre/dist/color-recipes-explorer-frontend;
root /usr/bin/;
include /etc/nginx/mime.types;
location / {

View File

@ -3,53 +3,66 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.conf.json",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"browser": {
"fs": false
},
"dependencies": {
"@angular/animations": "~11.2.10",
"@angular/cdk": "^11.2.11",
"@angular/common": "~11.2.10",
"@angular/compiler": "~11.2.10",
"@angular/core": "~11.2.10",
"@angular/forms": "~11.2.10",
"@angular/material": "^11.2.9",
"@angular/platform-browser": "~11.2.10",
"@angular/platform-browser-dynamic": "~11.2.10",
"@angular/router": "~11.2.10",
"@mdi/angular-material": "^5.7.55",
"@angular/animations": "~12.2.14",
"@angular/cdk": "^12.2.13",
"@angular/common": "~12.2.14",
"@angular/compiler": "~12.2.14",
"@angular/core": "~12.2.14",
"@angular/forms": "~12.2.14",
"@angular/material": "^12.2.13",
"@angular/platform-browser": "~12.2.14",
"@angular/platform-browser-dynamic": "~12.2.14",
"@angular/router": "~12.2.14",
"@js-joda/core": "^4.3.1",
"@mdi/angular-material": "^6.5.95",
"bootstrap": "^4.5.2",
"copy-webpack-plugin": "^6.2.1",
"js-joda": "^1.11.0",
"copy-webpack-plugin": "^10.0.0",
"jwt-decode": "^3.1.2",
"material-design-icons": "^3.0.1",
"ngx-material-file-input": "^2.1.1",
"rxjs": "~6.5.4",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
"rxjs": "^7.4.0",
"tslib": "^2.3.1",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.1102.9",
"@angular/cli": "^11.2.11",
"@angular/compiler-cli": "~11.2.10",
"@angular/language-service": "~11.2.10",
"@angular-devkit/build-angular": "^12.2.13",
"@angular-eslint/builder": "4.3.0",
"@angular-eslint/eslint-plugin": "4.3.0",
"@angular-eslint/eslint-plugin-template": "4.3.0",
"@angular-eslint/schematics": "4.3.0",
"@angular-eslint/template-parser": "4.3.0",
"@angular/cli": "^12.2.13",
"@angular/compiler-cli": "~12.2.14",
"@angular/language-service": "~12.2.14",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"@typescript-eslint/eslint-plugin": "4.16.1",
"@typescript-eslint/parser": "4.16.1",
"eslint": "^8.3.0",
"eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-prefer-arrow": "latest",
"jasmine-core": "^3.10.1",
"jasmine-spec-reporter": "^7.0.0",
"karma": "~6.3.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.7"
"ts-node": "^10.4.0",
"typescript": "~4.3.5"
}
}

24
src/_variables.scss Normal file
View File

@ -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;

View File

@ -3,12 +3,11 @@ import {Routes, RouterModule} from '@angular/router'
import {CatalogComponent} from './pages/catalog/catalog.component'
import {AdministrationComponent} from './pages/administration/administration.component'
import {MiscComponent} from './pages/others/misc.component'
import {CreConfigEditor} from './modules/configuration/config'
import {CreConfigEditor} from './modules/configuration/config-editor'
const routes: Routes = [{
path: 'color',
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule)
loadChildren: () => import('./modules/recipes/recipes.module').then(m => m.RecipesModule)
}, {
path: 'account',
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)

View File

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

View File

@ -1,10 +1,18 @@
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {NgModule} from '@angular/core'
import {RouterModule, Routes} from '@angular/router'
import {LogoutComponent} from './pages/logout/logout.component'
import {Login} from './accounts'
import {LoginComponent} from './pages/login/login.component';
import {LogoutComponent} from "./pages/logout/logout.component";
const routes: Routes = [{path: 'login', component: LoginComponent}, {path: 'logout', component: LogoutComponent}, {path: '', redirectTo: 'login'}];
const routes: Routes = [{
path: 'login',
component: Login
}, {
path: 'logout',
component: LogoutComponent
}, {
path: '',
redirectTo: 'login'
}]
@NgModule({
imports: [RouterModule.forChild(routes)],

View File

@ -1,18 +1,25 @@
import {NgModule} from '@angular/core';
import {NgModule} from '@angular/core'
import {AccountsRoutingModule} from './accounts-routing.module';
import {LoginComponent} from './pages/login/login.component';
import {SharedModule} from "../shared/shared.module";
import {LogoutComponent} from './pages/logout/logout.component';
import {CommonModule} from "@angular/common";
import {BrowserModule} from "@angular/platform-browser";
import {AccountsRoutingModule} from './accounts-routing.module'
import {LoginComponent} from './pages/login/login.component'
import {SharedModule} from '../shared/shared.module'
import {LogoutComponent} from './pages/logout/logout.component'
import {Login} from './accounts'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
@NgModule({
declarations: [LoginComponent, LogoutComponent],
declarations: [
LoginComponent,
LogoutComponent,
Login
],
imports: [
SharedModule,
AccountsRoutingModule,
CreInputsModule,
CreButtonsModule,
]
})
export class AccountsModule {

View File

@ -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
}
}
}

View File

@ -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>

View File

@ -31,7 +31,7 @@ export class LoginComponent extends ErrorHandlingComponent implements OnInit {
ngOnInit(): void {
this.errorService.activeErrorHandler = this
if (this.accountService.isLoggedIn()) {
if (this.appState.isAuthenticated) {
this.router.navigate(['/color'])
}
@ -44,10 +44,12 @@ export class LoginComponent extends ErrorHandlingComponent implements OnInit {
}
submit() {
this.accountService.login(
this.idFormControl.value,
this.passwordFormControl.value,
() => this.router.navigate(['/color'])
this.subscribe(
this.accountService.login(
this.idFormControl.value,
this.passwordFormControl.value
),
response => console.log(response)
)
}
}

View File

@ -1,28 +1,35 @@
import {Component, OnInit} from '@angular/core';
import {Component} from '@angular/core';
import {AccountService} from "../../services/account.service";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
import {AppState} from "../../../shared/app-state";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {ErrorService} from "../../../shared/service/error.service";
@Component({
selector: 'cre-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.sass']
})
export class LogoutComponent implements OnInit {
export class LogoutComponent extends SubscribingComponent {
constructor(
private accountService: AccountService,
private router: Router
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit(): void {
if (!this.accountService.isLoggedIn()) {
this.router.navigate(['/account/login'])
if (!this.appState.isAuthenticated) {
this.urlUtils.navigateTo('/account/login')
}
this.accountService.logout(() => {
this.router.navigate(['/account/login'])
})
this.subscribeAndNavigate(
this.accountService.logout(),
'/account/login'
)
}
}

View File

@ -1,14 +1,14 @@
import {Injectable, OnDestroy} from '@angular/core'
import {Subject} from 'rxjs'
import {Observable, Subject} from 'rxjs'
import {take, takeUntil} from 'rxjs/operators'
import {AppState} from '../../shared/app-state'
import {HttpClient, HttpResponse} from '@angular/common/http'
import {environment} from '../../../../environments/environment'
import {ApiService} from '../../shared/service/api.service'
import {User, Permission} from '../../shared/model/user'
import {Permission, User} from '../../shared/model/user'
import {ErrorService} from '../../shared/service/error.service'
import {globalLoadingWheel} from '../../shared/components/loading-wheel/loading-wheel.component'
import {AlertService} from '../../shared/service/alert.service'
import {JwtService} from "./jwt.service";
@Injectable({
providedIn: 'root'
@ -20,6 +20,7 @@ export class AccountService implements OnDestroy {
private http: HttpClient,
private api: ApiService,
private appState: AppState,
private jwtService: JwtService,
private errorService: ErrorService,
private alertService: AlertService
) {
@ -30,20 +31,16 @@ export class AccountService implements OnDestroy {
this.destroy$.complete()
}
isLoggedIn(): boolean {
return this.appState.isAuthenticated
}
checkAuthenticationStatus() {
if (!this.appState.authenticatedUser) {
if (!this.appState.isAuthenticated) {
// Try to get current default group user
this.http.get<User>(`${environment.apiUrl}/user/current`, {withCredentials: true})
this.http.get<User>(`${environment.apiUrl}/user/group/currentuser`, {withCredentials: true})
.pipe(
take(1),
takeUntil(this.destroy$),
).subscribe(
{
next: user => this.appState.authenticatedUser = user,
next: user => this.appState.authenticateGroupUser(user),
error: err => {
if (err.status === 404 || err.status === 403) {
console.warn('No default user is defined on this computer')
@ -55,67 +52,74 @@ export class AccountService implements OnDestroy {
}
}
login(id: number, password: string, success: () => void) {
const loginForm = {id, password}
globalLoadingWheel.show()
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
login(userId: number, password: string): Observable<any> {
const subject = new Subject<void>()
this.http.post<any>(`${environment.apiUrl}/login`, {id: userId, password}, {
withCredentials: true,
observe: 'response' as 'body'
})
.pipe(
take(1),
takeUntil(this.destroy$)
)
.subscribe({
next: (response: HttpResponse<any>) => {
this.appState.authenticationExpiration = parseInt(response.headers.get('X-Authentication-Expiration'))
this.appState.isAuthenticated = true
this.setLoggedInUserFromApi()
success()
},
error: err => {
globalLoadingWheel.hide()
if (err.status === 401 || err.status === 403) {
this.alertService.pushError('Les identifiants entrés sont invalides')
} else {
this.errorService.handleError(err)
}
}
})
}
logout(success: () => void) {
this.api.get<void>('/logout', true).pipe(
}).pipe(
take(1),
takeUntil(this.destroy$)
)
.subscribe({
next: () => {
this.appState.resetAuthenticatedUser()
this.checkAuthenticationStatus()
success()
},
error: err => this.errorService.handleError(err)
})
).subscribe({
next: (response: HttpResponse<void>) => {
this.loginUser(response)
subject.next()
subject.complete()
},
error: error => {
if (error.status === 403) {
this.alertService.pushError('Les identifiants entrés sont invalides')
} else {
this.errorService.handleError(error)
}
subject.next()
subject.complete()
}
})
return subject
}
private loginUser(response: HttpResponse<void>) {
const authorization = response.headers.get("Authorization")
const user = this.jwtService.parseUser(authorization)
this.appState.authenticateUser(user)
}
logout(): Observable<void> {
const subject = new Subject<void>()
this.api.get<void>('/logout').pipe(
take(1),
takeUntil(this.destroy$)
).subscribe({
next: () => {
this.logoutUser()
subject.next()
subject.complete()
},
error: error => {
this.errorService.handleError(error)
subject.next()
subject.complete()
}
})
return subject
}
private logoutUser() {
this.appState.resetAuthenticatedUser()
this.checkAuthenticationStatus()
}
hasPermission(permission: Permission): boolean {
return this.appState.authenticatedUser && this.appState.authenticatedUser.permissions.indexOf(permission) >= 0
}
private setLoggedInUserFromApi() {
this.api.get<User>('/user/current', true)
.pipe(
take(1),
takeUntil(this.destroy$)
)
.subscribe({
next: user => {
this.appState.authenticatedUser = user
// At this point the loading wheel should be visible
globalLoadingWheel.hide()
},
error: err => this.errorService.handleError(err)
})
}
}

View File

@ -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)
}
}

View File

@ -1,39 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ListComponent} from "./pages/list/list.component";
import {AddComponent} from "./pages/add/add.component";
import {EditComponent} from "./pages/edit/edit.component";
import {ExploreComponent} from "./pages/explore/explore.component";
import {MixEditComponent} from "./pages/mix/mix-edit/mix-edit.component";
import {MixAddComponent} from "./pages/mix/mix-add/mix-add.component";
const routes: Routes = [{
path: 'list',
component: ListComponent
}, {
path: 'add',
component: AddComponent
}, {
path: 'edit/:id',
component: EditComponent
}, {
path: 'add/mix/:recipeId',
component: MixAddComponent
}, {
path: 'edit/mix/:recipeId/:id',
component: MixEditComponent
}, {
path: 'explore/:id',
component: ExploreComponent
}, {
path: '',
pathMatch: 'full',
redirectTo: 'list'
}]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ColorsRoutingModule {
}

View File

@ -1,38 +0,0 @@
import {NgModule} from '@angular/core'
import {ColorsRoutingModule} from './colors-routing.module'
import {SharedModule} from '../shared/shared.module'
import {ListComponent} from './pages/list/list.component'
import {AddComponent} from './pages/add/add.component'
import {EditComponent} from './pages/edit/edit.component'
import {MatExpansionModule} from '@angular/material/expansion'
import {FormsModule} from '@angular/forms'
import {ExploreComponent} from './pages/explore/explore.component'
import {RecipeInfoComponent} from './components/recipe-info/recipe-info.component'
import {MixTableComponent} from './components/mix-table/mix-table.component'
import {StepListComponent} from './components/step-list/step-list.component'
import {StepTableComponent} from './components/step-table/step-table.component'
import {MixEditorComponent} from './components/mix-editor/mix-editor.component'
import {UnitSelectorComponent} from './components/unit-selector/unit-selector.component'
import {MixAddComponent} from './pages/mix/mix-add/mix-add.component'
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component'
import {ImagesEditorComponent} from './components/images-editor/images-editor.component'
import {MixesCardComponent} from './components/mixes-card/mixes-card.component'
import {MatSortModule} from '@angular/material/sort'
@NgModule({
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent, ImagesEditorComponent, MixesCardComponent],
exports: [
UnitSelectorComponent
],
imports: [
ColorsRoutingModule,
SharedModule,
MatExpansionModule,
FormsModule,
MatSortModule
]
})
export class ColorsModule {
}

View File

@ -1,136 +0,0 @@
<mat-card *ngIf="recipe && (!editionMode || mix)" class="x-centered mt-5">
<mat-card-header>
<mat-card-title *ngIf="!editionMode">Création d'un mélange pour la recette {{recipe.company.name}}
- {{recipe.name}}</mat-card-title>
<mat-card-title *ngIf="editionMode">Modification du mélange {{mix.mixType.name}} de la
recette {{recipe.company.name}} - {{recipe.name}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-form-field>
<mat-label>Nom</mat-label>
<input matInput type="text" [formControl]="nameControl"/>
<mat-icon svgIcon="form-textbox" matSuffix></mat-icon>
</mat-form-field>
<mat-form-field>
<mat-label>Type de produit</mat-label>
<mat-select [formControl]="materialTypeControl">
<mat-option
*ngFor="let materialType of (materialTypes$ | async)"
[value]="materialType.id">
{{materialType.name}}
</mat-option>
</mat-select>
</mat-form-field>
<div class="mix-materials-wrapper">
<ng-container *ngTemplateOutlet="mixEditor"></ng-container>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" routerLink="/color/edit/{{recipeId}}">Retour</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="deleteConfirmBox.show()">
Supprimer
</button>
<button mat-raised-button color="accent" [disabled]="!form.valid" (click)="submit()">Enregistrer</button>
</mat-card-actions>
</mat-card>
<ng-template #mixEditor>
<table #matTable mat-table [dataSource]="mixMaterials">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let mixMaterial">
{{mixMaterial.position}}
</td>
</ng-container>
<ng-container matColumnDef="buttonsPosition">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mixMaterial; let i = index">
<ng-container *ngIf="(!hoveredMixMaterial && i === 0) || hoveredMixMaterial === mixMaterial">
<button
mat-mini-fab
color="primary"
class="mr-1"
[disabled]="mixMaterial.position <= 1"
(click)="decreasePosition(mixMaterial, matTable)">
<mat-icon svgIcon="arrow-up"></mat-icon>
</button>
<button
mat-mini-fab
color="primary"
[disabled]="mixMaterial.position >= mixMaterials.length"
(click)="increasePosition(mixMaterial, matTable)">
<mat-icon svgIcon="arrow-down"></mat-icon>
</button>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="material">
<th mat-header-cell *matHeaderCellDef>Produit</th>
<td mat-cell *matCellDef="let mixMaterial">
<mat-form-field *ngIf="materials">
<mat-select
[value]="mixMaterial.materialId"
(valueChange)="setMixMaterialMaterial(mixMaterial, $event)">
<mat-option
*ngFor="let material of sortedMaterials(getAvailableMaterials(mixMaterial))"
[value]="material.id">
{{materialDisplayName(material)}}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let mixMaterial">
<mat-form-field>
<input matInput type="number" step="0.001" [(ngModel)]="mixMaterial.quantity"/>
</mat-form-field>
</td>
</ng-container>
<ng-container matColumnDef="units">
<th mat-header-cell *matHeaderCellDef>Unités</th>
<td mat-cell *matCellDef="let mixMaterial" class="units-wrapper">
<ng-container *ngIf="materials">
<ng-container *ngIf="mixMaterial.isPercents">
<p>%</p>
</ng-container>
<ng-container *ngIf="!mixMaterial.isPercents">
<ng-container *ngIf="!hoveredMixMaterial || hoveredMixMaterial != mixMaterial">
<span>{{units}}</span>
</ng-container>
<ng-container *ngIf="hoveredMixMaterial && hoveredMixMaterial == mixMaterial">
<cre-unit-selector [(unit)]="units" [showLabel]="false" [short]="true"></cre-unit-selector>
</ng-container>
</ng-container>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="buttonRemove">
<th mat-header-cell *matHeaderCellDef>
<button mat-raised-button color="accent" (click)="addRow()">Ajouter</button>
</th>
<td mat-cell *matCellDef="let mixMaterial; let i = index">
<ng-container *ngIf="hoveredMixMaterial && hoveredMixMaterial == mixMaterial">
<button mat-raised-button color="warn" (click)="removeRow(i)">Retirer</button>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let mixMaterial; columns: columns" (mouseover)="hoveredMixMaterial = mixMaterial"></tr>
</table>
</ng-template>
<cre-confirm-box
*ngIf="editionMode && mix"
#deleteConfirmBox
message="Voulez-vous vraiment supprimer le mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}}"
(confirm)="delete()">
</cre-confirm-box>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +0,0 @@
<div *ngIf="recipe">
<div class="action-bar backward">
<div class="d-flex flex-column">
<div class="mt-1 pb-2">
<button mat-raised-button color="primary" routerLink="/color/list">Retour</button>
<button
mat-raised-button
color="accent"
[disabled]="editComponent.form && editComponent.form.invalid"
(click)="submit(editComponent, stepTable)">
Enregistrer
</button>
<button
mat-raised-button
color="warn"
(click)="confirmBoxComponent.show()">
Supprimer
</button>
</div>
<mat-form-field>
<mat-label>Unités</mat-label>
<mat-select [value]="unitConstants.UNIT_MILLILITER" (selectionChange)="changeUnits($event.value)">
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
<mat-option [value]="unitConstants.UNIT_GALLON">Gallons</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex-grow-1"></div>
</div>
<div class="recipe-wrapper d-flex flex-row justify-content-around align-items-start flex-wrap">
<div>
<cre-entity-edit
#editComponent
title="Modifier la couleur {{recipe.name}}"
deleteConfirmMessage="Voulez-vous vraiment supprimer la couleur {{recipe.name}}?"
[entity]="recipe"
[formFields]="formFields"
[disableButtons]="true"
[noTopMargin]="true">
</cre-entity-edit>
</div>
<div class="recipe-mixes-wrapper">
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
</div>
<div>
<cre-step-table
#stepTable
[recipe]="recipe"
[groups$]="groups$"
[selectedGroupId]="loggedInUserGroupId">
</cre-step-table>
</div>
<div>
<cre-images-editor #imagesEditor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
</div>
</div>
</div>
<cre-confirm-box
#confirmBoxComponent
message="Voulez-vous vraiment supprimer la couleur {{recipe?.name}}?"
(confirm)="delete()">
</cre-confirm-box>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
mat-expansion-panel
width: 60rem
margin: 20px auto
.button-add
margin-top: .8rem
.recipe-color-circle
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<div
class="cre-config"
[class.cre-readonly-config]="readOnly"
[attr.title]="tooltip">
<ng-content></ng-content>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -1,8 +1,6 @@
<mat-card class="w-50 x-centered">
<mat-card-header>
<mat-card-title>
<ng-content select="cre-config-label"></ng-content>
</mat-card-title>
<mat-card-title>{{label}}</mat-card-title>
</mat-card-header>
<mat-card-content [class.no-action]="!hasActions">
<ng-content select="cre-config-list"></ng-content>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,32 +1,35 @@
import {NgModule} from '@angular/core'
import {
CreConfig,
CreConfigLabel,
CreConfigEditor,
CreConfigSection,
CreImageConfig,
CreConfigList,
CreBoolConfig,
CreConfigActions,
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig
CreConfigContainer,
CreConfigList,
CreConfigSection,
CreDateConfig,
CreImageConfig,
CrePeriodConfig,
CreSecureConfig,
CreTextConfig
} from './config'
import {SharedModule} from '../shared/shared.module'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreConfigEditor} from './config-editor'
@NgModule({
declarations: [
CreConfigLabel,
CreConfigTooltip,
CreConfigEditor,
CreConfig,
CreImageConfig,
CreConfigSection,
CreConfigList,
CreConfigActions,
CreConfigSection,
CreConfigContainer,
CreTextConfig,
CreImageConfig,
CreBoolConfig,
CrePeriodConfig,
CreDateConfig
CreDateConfig,
CreSecureConfig,
CreConfigEditor
],
imports: [
SharedModule,
@ -35,4 +38,5 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
CreButtonsModule
]
})
export class ConfigModule { }
export class ConfigModule {
}

View File

@ -1,10 +1,10 @@
mat-hint
font-size: .8em
cre-config
cre-config-container
display: block
cre-input.has-hint
.cre-config:not(.cre-editable-config)
margin-bottom: 1em
mat-hint

View File

@ -3,7 +3,6 @@ import {
Component,
ContentChild,
Directive,
ElementRef,
EventEmitter,
Input,
Output,
@ -11,46 +10,13 @@ import {
ViewEncapsulation
} from '@angular/core'
import {ConfigService} from '../shared/service/config.service'
import {Config} from '../shared/model/config.model'
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
import {Config, ConfigControl} from '../shared/model/config.model'
import {SubscribingComponent} from '../shared/components/subscribing.component'
import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
import {FormControl, Validators} from '@angular/forms'
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
import {environment} from '../../../environments/environment'
@Directive({
selector: 'cre-config-label'
})
export class CreConfigLabel implements AfterViewInit {
content: string
constructor(
private element: ElementRef
) {
}
ngAfterViewInit(): void {
this.content = this.element.nativeElement.innerHTML
}
}
@Directive({
selector: 'cre-config-tooltip'
})
export class CreConfigTooltip implements AfterViewInit {
content: string
constructor(
private element: ElementRef
) {
}
ngAfterViewInit(): void {
this.content = this.element.nativeElement.innerHTML
}
}
import {formatDate, formatDateTime, getConfiguredImageUrl, getFileUrl, readFile} from '../shared/utils/utils'
import {AbstractControl} from '@angular/forms'
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@Directive({
selector: 'cre-config-list'
@ -70,6 +36,8 @@ export class CreConfigActions {
templateUrl: 'config-section.html'
})
export class CreConfigSection {
@Input() label: string
@ContentChild(CreConfigActions) actions: CreConfigActions
get hasActions(): boolean {
@ -78,17 +46,25 @@ export class CreConfigSection {
}
@Component({
selector: 'cre-config',
templateUrl: 'config.html',
styleUrls: ['config.sass']
selector: 'cre-config-container',
templateUrl: 'config-container.html',
styleUrls: ['config.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreConfig extends SubscribingComponent {
@Input() config: { key: string, control: FormControl }
export class CreConfigContainer {
@Input() configuration?: Config
@Input() tooltip: string
@ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel
@ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip
get readOnly(): boolean {
return !this.configuration?.editable ?? true
}
}
configuration: Config | null
@Directive()
abstract class _CreConfigBase extends SubscribingComponent {
@Input() configControl: ConfigControl
@Input() label: string
@Input() tooltip?: string
constructor(
private configService: ConfigService,
@ -99,41 +75,103 @@ export class CreConfig extends SubscribingComponent {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.subscribe(
this.configService.get(this.config.key),
config => this.setConfig(config)
)
get config(): Config {
return this.configControl.config
}
setConfig(config: Config) {
this.configuration = config
this.config.control.setValue(config.content)
if (!config.editable) {
this.config.control.disable()
}
get control(): AbstractControl {
return this.configControl.control
}
get lastUpdated(): string {
return 'Dernière mise à jour: ' + formatDateTime(this.configuration.lastUpdated)
return 'Dernière mise à jour: ' + formatDateTime(this.config.lastUpdated)
}
get inputHint(): string {
return this.config?.editable ? this.lastUpdated : null
}
}
@Directive()
abstract class _CreTextConfigBase extends _CreConfigBase {
private static readonly REQUIRE_RESTART_ICON = 'alert'
private static readonly REQUIRE_RESTART_ICON_TITLE = 'Requiert un redémarrage'
get inputIcon(): string {
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON : null
}
get inputIconTitle(): string {
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON_TITLE : null
}
}
@Component({
selector: 'cre-text-config',
templateUrl: 'config-text.html',
styleUrls: ['config.sass']
})
export class CreTextConfig extends _CreTextConfigBase {
}
@Component({
selector: 'cre-image-config',
templateUrl: 'image.html',
templateUrl: 'config-image.html',
styleUrls: ['config.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreImageConfig extends CreConfig {
export class CreImageConfig extends _CreConfigBase {
@Input() previewWidth: string | null
@Output() invalidFormat = new EventEmitter<void>()
updatedImage: any | null
updateImage(file: File): any {
readFile(file, (content) => this.updatedImage = content)
}
get imageUrl(): string {
const path = this.config.key == Config.INSTANCE_ICON_SET ? 'icon' : 'logo'
return getConfiguredImageUrl(path)
}
}
@Component({
selector: 'cre-bool-config',
templateUrl: 'config-bool.html'
})
export class CreBoolConfig extends _CreConfigBase {
}
@Component({
selector: 'cre-period-config',
templateUrl: 'config-period.html'
})
export class CrePeriodConfig extends _CreConfigBase {
}
@Component({
selector: 'cre-date-config',
templateUrl: 'config-date.html'
})
export class CreDateConfig extends _CreTextConfigBase implements AfterViewInit {
ngAfterViewInit(): void {
this.control.setValue(formatDate(this.config.content))
}
}
@Component({
selector: 'cre-secure-config',
templateUrl: 'config-secure.html'
})
export class CreSecureConfig extends _CreTextConfigBase {
@ViewChild(CrePromptDialog) dialog: CrePromptDialog
@Input() buttonLabel: string
private initialValue: string | null
constructor(
configService: ConfigService,
errorService: ErrorService,
@ -143,113 +181,12 @@ export class CreImageConfig extends CreConfig {
super(configService, errorService, activatedRoute, router)
}
updateImage(file: File): any {
readFile(file, (content) => this.updatedImage = content)
onOpen() {
this.initialValue = this.control.value
this.dialog.show()
}
get configuredImageUrl(): string {
return getFileUrl(this.configuration.content)
}
}
@Component({
selector: 'cre-bool-config',
templateUrl: 'bool.html'
})
export class CreBoolConfig extends CreConfig {
setConfig(config: Config) {
super.setConfig(config)
this.config.control.setValue(config.content === 'true')
}
}
@Component({
selector: 'cre-period-config',
templateUrl: 'period.html'
})
export class CrePeriodConfig extends CreConfig {
}
@Component({
selector: 'cre-date-config',
templateUrl: 'date.html'
})
export class CreDateConfig extends CreConfig {
setConfig(config: Config) {
super.setConfig(config);
this.config.control.setValue(formatDate(config.content))
}
}
@Component({
selector: 'cre-config-editor',
templateUrl: 'editor.html'
})
export class CreConfigEditor extends ErrorHandlingComponent {
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
keys = {
INSTANCE_NAME: Config.INSTANCE_NAME,
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
INSTANCE_URL: Config.INSTANCE_URL,
DATABASE_URL: Config.DATABASE_URL,
DATABASE_USER: Config.DATABASE_USER,
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
DATABASE_VERSION: Config.DATABASE_VERSION,
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
JAVA_VERSION: Config.JAVA_VERSION,
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
}
controls = new Map<string, FormControl>()
emergencyMode: string | null
constructor(
private configService: ConfigService,
errorService: ErrorService,
activatedRoute: ActivatedRoute,
router: Router
) {
super(errorService, activatedRoute, router)
for (let key in this.keys) {
this.controls[this.keys[key]] = new FormControl(null, Validators.required)
}
}
ngOnInit() {
this.subscribe(
this.configService.get(Config.EMERGENCY_MODE),
config => {
this.emergencyMode = config.content
}
)
}
getConfig(key: string) {
return {key, control: this.controls[key]}
}
save() {
this.subscribe(
this.configService.set(this.controls),
() => this.reload()
)
}
restart() {
this.subscribe(
this.configService.restart(),
() => this.restartConfirmBox.show()
)
}
reload() {
window.location.reload()
onCancel() {
this.control.setValue(this.initialValue)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-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>

View File

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

View File

@ -1,4 +1,4 @@
@import '../../../../../custom-theme'
@import "~src/variables"
mat-expansion-panel
width: 48rem

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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