diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..642937a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/node_modules +.gitignore +.dockerignore +Dockerfile +docker-compose.yml +package-lock.json diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..11396c2 --- /dev/null +++ b/.drone.yml @@ -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 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..88b1a2e --- /dev/null +++ b/.eslintrc.json @@ -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": {} + } + ] +} diff --git a/.gitignore b/.gitignore index 2e789dc..ccbebdb 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ speed-measure-plugin*.json .history/* # misc +/.angular/cache /.sass-cache /connect.lock /coverage diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 74cbfb6..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -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" - - diff --git a/Dockerfile b/Dockerfile index 464c4fc..1325a16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,29 @@ -FROM nginx:mainline-alpine - -WORKDIR /usr/bin/cre/ - -ARG ARTIFACT_NAME=ColorRecipesExplorer-ng -COPY $ARTIFACT_NAME.zip . -COPY nginx.conf /etc/nginx/nginx.conf +FROM alpine:latest AS build +WORKDIR /usr/src/ RUN apk update -RUN apk add --no-cache zip +RUN apk add --no-cache nodejs npm -RUN unzip $ARTIFACT_NAME.zip -RUN rm $ARTIFACT_NAME.zip +RUN npm install -g typescript@4.0.7 && \ + npm install -g @angular/cli@11.2.9 || true --fo -EXPOSE 80 +ENV NG_CLI_ANALYTICS=ci + +COPY . . + +ARG ANGULAR_CONFIGURATION=production + +RUN npm install --force +RUN ng build --configuration=$ANGULAR_CONFIGURATION --stats-json --source-map=false + +FROM nginx:mainline-alpine + +WORKDIR /usr/bin/ + +COPY nginx.conf /etc/nginx/nginx.conf +COPY --from=build /usr/src/dist/color-recipes-explorer-frontend/ . + +ARG CRE_PORT=80 +EXPOSE $CRE_PORT CMD ["nginx", "-g", "daemon off;"] diff --git a/angular.json b/angular.json index 452f6d1..2fab785 100644 --- a/angular.json +++ b/angular.json @@ -22,18 +22,27 @@ "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", - "aot": true, "assets": [ "src/favicon.ico", "src/assets", - { "glob": "mdi.svg", "input": "./node_modules/@mdi/angular-material", "output": "./assets"} + { + "glob": "mdi.svg", + "input": "./node_modules/@mdi/angular-material", + "output": "./assets" + } ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.sass" ], - "scripts": [] + "scripts": [], + "vendorChunk": true, + "extractLicenses": false, + "buildOptimizer": false, + "sourceMap": true, + "optimization": false, + "namedChunks": true }, "configurations": { "production": { @@ -63,7 +72,8 @@ } ] } - } + }, + "defaultConfiguration": "" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", @@ -100,15 +110,11 @@ } }, "lint": { - "builder": "@angular-devkit/build-angular:tslint", + "builder": "@angular-eslint/builder:lint", "options": { - "tsConfig": [ - "tsconfig.app.json", - "tsconfig.spec.json", - "e2e/tsconfig.json" - ], - "exclude": [ - "**/node_modules/**" + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.html" ] } }, @@ -127,5 +133,8 @@ } } }, - "defaultProject": "color-recipes-explorer-frontend" + "defaultProject": "color-recipes-explorer-frontend", + "cli": { + "defaultCollection": "@angular-eslint/schematics" + } } diff --git a/docker-compose.yml b/docker-compose.yml index 135ab08..b6c8985 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/ng.Dockerfile b/ng.Dockerfile deleted file mode 100644 index 92c9ee8..0000000 --- a/ng.Dockerfile +++ /dev/null @@ -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 . . diff --git a/nginx.conf b/nginx.conf index 1f73fad..dea68be 100644 --- a/nginx.conf +++ b/nginx.conf @@ -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 / { diff --git a/package.json b/package.json index 4375f04..572b917 100644 --- a/package.json +++ b/package.json @@ -3,53 +3,66 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve --proxy-config proxy.conf.json", + "start": "ng serve --host 0.0.0.0 --proxy-config proxy.conf.json", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, + "browser": { + "fs": false + }, "dependencies": { - "@angular/animations": "~11.2.10", - "@angular/cdk": "^11.2.11", - "@angular/common": "~11.2.10", - "@angular/compiler": "~11.2.10", - "@angular/core": "~11.2.10", - "@angular/forms": "~11.2.10", - "@angular/material": "^11.2.9", - "@angular/platform-browser": "~11.2.10", - "@angular/platform-browser-dynamic": "~11.2.10", - "@angular/router": "~11.2.10", - "@mdi/angular-material": "^5.7.55", + "@angular/animations": "~12.2.14", + "@angular/cdk": "^12.2.13", + "@angular/common": "~12.2.14", + "@angular/compiler": "~12.2.14", + "@angular/core": "~12.2.14", + "@angular/forms": "~12.2.14", + "@angular/material": "^12.2.13", + "@angular/platform-browser": "~12.2.14", + "@angular/platform-browser-dynamic": "~12.2.14", + "@angular/router": "~12.2.14", + "@js-joda/core": "^4.3.1", + "@mdi/angular-material": "^6.5.95", "bootstrap": "^4.5.2", - "copy-webpack-plugin": "^6.2.1", - "js-joda": "^1.11.0", + "copy-webpack-plugin": "^10.0.0", + "jwt-decode": "^3.1.2", "material-design-icons": "^3.0.1", "ngx-material-file-input": "^2.1.1", - "rxjs": "~6.5.4", - "tslib": "^2.0.0", - "zone.js": "~0.10.2" + "rxjs": "^7.4.0", + "tslib": "^2.3.1", + "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.1102.9", - "@angular/cli": "^11.2.11", - "@angular/compiler-cli": "~11.2.10", - "@angular/language-service": "~11.2.10", + "@angular-devkit/build-angular": "^12.2.13", + "@angular-eslint/builder": "4.3.0", + "@angular-eslint/eslint-plugin": "4.3.0", + "@angular-eslint/eslint-plugin-template": "4.3.0", + "@angular-eslint/schematics": "4.3.0", + "@angular-eslint/template-parser": "4.3.0", + "@angular/cli": "^12.2.13", + "@angular/compiler-cli": "~12.2.14", + "@angular/language-service": "~12.2.14", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "~2.0.3", "@types/node": "^12.11.1", - "codelyzer": "^6.0.0", - "jasmine-core": "~3.6.0", - "jasmine-spec-reporter": "~5.0.0", + "@typescript-eslint/eslint-plugin": "4.16.1", + "@typescript-eslint/parser": "4.16.1", + "eslint": "^8.3.0", + "eslint-plugin-import": "latest", + "eslint-plugin-jsdoc": "latest", + "eslint-plugin-prefer-arrow": "latest", + "jasmine-core": "^3.10.1", + "jasmine-spec-reporter": "^7.0.0", "karma": "~6.3.2", "karma-chrome-launcher": "~3.1.0", "karma-coverage-istanbul-reporter": "~3.0.2", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "protractor": "~7.0.0", - "ts-node": "~8.3.0", - "tslint": "~6.1.0", - "typescript": "~4.0.7" + "ts-node": "^10.4.0", + "typescript": "~4.3.5" } } diff --git a/src/_variables.scss b/src/_variables.scss new file mode 100644 index 0000000..c616181 --- /dev/null +++ b/src/_variables.scss @@ -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; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 24a2458..3d9f34f 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7f5758f..0d6e334 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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() { diff --git a/src/app/modules/accounts/accounts-routing.module.ts b/src/app/modules/accounts/accounts-routing.module.ts index 843486b..006c93f 100644 --- a/src/app/modules/accounts/accounts-routing.module.ts +++ b/src/app/modules/accounts/accounts-routing.module.ts @@ -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)], diff --git a/src/app/modules/accounts/accounts.module.ts b/src/app/modules/accounts/accounts.module.ts index a935cd7..c8a05ec 100644 --- a/src/app/modules/accounts/accounts.module.ts +++ b/src/app/modules/accounts/accounts.module.ts @@ -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 { diff --git a/src/app/modules/accounts/accounts.ts b/src/app/modules/accounts/accounts.ts new file mode 100644 index 0000000..10f850b --- /dev/null +++ b/src/app/modules/accounts/accounts.ts @@ -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 + } + } +} diff --git a/src/app/modules/accounts/login.html b/src/app/modules/accounts/login.html new file mode 100644 index 0000000..7ec17e8 --- /dev/null +++ b/src/app/modules/accounts/login.html @@ -0,0 +1,28 @@ + + Connexion au système + + + + Le numéro d'utilisateur doit être un nombre + + + + + + + + + Connexion + + + diff --git a/src/app/modules/accounts/pages/login/login.component.ts b/src/app/modules/accounts/pages/login/login.component.ts index 1802a7c..919bed0 100644 --- a/src/app/modules/accounts/pages/login/login.component.ts +++ b/src/app/modules/accounts/pages/login/login.component.ts @@ -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) ) } } diff --git a/src/app/modules/accounts/pages/logout/logout.component.ts b/src/app/modules/accounts/pages/logout/logout.component.ts index 8b180bd..a1433c1 100644 --- a/src/app/modules/accounts/pages/logout/logout.component.ts +++ b/src/app/modules/accounts/pages/logout/logout.component.ts @@ -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' + ) } - } diff --git a/src/app/modules/accounts/services/account.service.ts b/src/app/modules/accounts/services/account.service.ts index dbf3362..066973a 100644 --- a/src/app/modules/accounts/services/account.service.ts +++ b/src/app/modules/accounts/services/account.service.ts @@ -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(`${environment.apiUrl}/user/current`, {withCredentials: true}) + this.http.get(`${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(`${environment.apiUrl}/login`, loginForm, { + login(userId: number, password: string): Observable { + const subject = new Subject() + + this.http.post(`${environment.apiUrl}/login`, {id: userId, password}, { withCredentials: true, observe: 'response' as 'body' - }) - .pipe( - take(1), - takeUntil(this.destroy$) - ) - .subscribe({ - next: (response: HttpResponse) => { - 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('/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) => { + 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) { + const authorization = response.headers.get("Authorization") + const user = this.jwtService.parseUser(authorization) + + this.appState.authenticateUser(user) + } + + logout(): Observable { + const subject = new Subject() + + this.api.get('/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/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) - }) - } } diff --git a/src/app/modules/accounts/services/jwt.service.ts b/src/app/modules/accounts/services/jwt.service.ts new file mode 100644 index 0000000..6f67366 --- /dev/null +++ b/src/app/modules/accounts/services/jwt.service.ts @@ -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) + } +} diff --git a/src/app/modules/colors/colors-routing.module.ts b/src/app/modules/colors/colors-routing.module.ts deleted file mode 100644 index 879d0a1..0000000 --- a/src/app/modules/colors/colors-routing.module.ts +++ /dev/null @@ -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 { -} diff --git a/src/app/modules/colors/colors.module.ts b/src/app/modules/colors/colors.module.ts deleted file mode 100644 index c994650..0000000 --- a/src/app/modules/colors/colors.module.ts +++ /dev/null @@ -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 { -} diff --git a/src/app/modules/colors/components/mix-editor/mix-editor.component.html b/src/app/modules/colors/components/mix-editor/mix-editor.component.html deleted file mode 100644 index a89926b..0000000 --- a/src/app/modules/colors/components/mix-editor/mix-editor.component.html +++ /dev/null @@ -1,136 +0,0 @@ - - - Création d'un mélange pour la recette {{recipe.company.name}} - - {{recipe.name}} - Modification du mélange {{mix.mixType.name}} de la - recette {{recipe.company.name}} - {{recipe.name}} - - - - Nom - - - - - Type de produit - - - {{materialType.name}} - - - - -
- -
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Position - {{mixMaterial.position}} - - - - - - Produit - - - - {{materialDisplayName(material)}} - - - - Quantité - - - - Unités - - -

%

-
- - - {{units}} - - - - - -
-
- - - - - -
-
- - - diff --git a/src/app/modules/colors/components/mix-editor/mix-editor.component.sass b/src/app/modules/colors/components/mix-editor/mix-editor.component.sass deleted file mode 100644 index 3eb5b9b..0000000 --- a/src/app/modules/colors/components/mix-editor/mix-editor.component.sass +++ /dev/null @@ -1,6 +0,0 @@ -mat-card - max-width: unset !important - -td.units-wrapper p - width: 3rem - margin-bottom: 0 diff --git a/src/app/modules/colors/components/mix-editor/mix-editor.component.ts b/src/app/modules/colors/components/mix-editor/mix-editor.component.ts deleted file mode 100644 index d4eee9e..0000000 --- a/src/app/modules/colors/components/mix-editor/mix-editor.component.ts +++ /dev/null @@ -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 - @ViewChild('deleteConfirmBox') deleteConfirmBox: ConfirmBoxComponent - - @Input() mixId: number | null - @Input() recipeId: number | null - @Input() materials: Material[] - - @Output() save = new EventEmitter() - - 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) { - this.updateMixMaterialPosition(mixMaterial, mixMaterial.position + 1) - this.sort(table) - } - - decreasePosition(mixMaterial: MixMaterialDto, table: MatTable) { - this.updateMixMaterialPosition(mixMaterial, mixMaterial.position - 1) - this.sort(table) - } - - sort(table: MatTable) { - 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) - } -} diff --git a/src/app/modules/colors/pages/add/add.component.html b/src/app/modules/colors/pages/add/add.component.html deleted file mode 100644 index f95fa2b..0000000 --- a/src/app/modules/colors/pages/add/add.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/src/app/modules/colors/pages/add/add.component.sass b/src/app/modules/colors/pages/add/add.component.sass deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/modules/colors/pages/add/add.component.ts b/src/app/modules/colors/pages/add/add.component.ts deleted file mode 100644 index e5bcf7e..0000000 --- a/src/app/modules/colors/pages/add/add.component.ts +++ /dev/null @@ -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}`) - ) - } -} diff --git a/src/app/modules/colors/pages/edit/edit.component.html b/src/app/modules/colors/pages/edit/edit.component.html deleted file mode 100644 index 81461f8..0000000 --- a/src/app/modules/colors/pages/edit/edit.component.html +++ /dev/null @@ -1,68 +0,0 @@ -
-
-
-
- - - -
- - Unités - - Millilitres - Litres - Gallons - - -
-
-
- -
-
- - -
- -
- -
- -
- - -
- -
- -
-
-
- - - diff --git a/src/app/modules/colors/pages/edit/edit.component.sass b/src/app/modules/colors/pages/edit/edit.component.sass deleted file mode 100644 index 2533b3c..0000000 --- a/src/app/modules/colors/pages/edit/edit.component.sass +++ /dev/null @@ -1,2 +0,0 @@ -.recipe-wrapper > div - margin: 0 3rem 3rem diff --git a/src/app/modules/colors/pages/edit/edit.component.ts b/src/app/modules/colors/pages/edit/edit.component.ts deleted file mode 100644 index ab663dc..0000000 --- a/src/app/modules/colors/pages/edit/edit.component.ts +++ /dev/null @@ -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() - 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): boolean { - let valid = true - steps.forEach((steps, _) => { - if (steps.find(s => s.position === 0)) { - valid = false - return - } - }) - return valid - } -} diff --git a/src/app/modules/colors/pages/explore/explore.component.html b/src/app/modules/colors/pages/explore/explore.component.html deleted file mode 100644 index 93b3b3e..0000000 --- a/src/app/modules/colors/pages/explore/explore.component.html +++ /dev/null @@ -1,86 +0,0 @@ -
- - -
-
-
- - - -
- -
- - - Groupe - - - {{group.name}} - - - -
-
- -
- - - Note - - -

{{selectedGroupNote}}

-
- -
- -
- - -
- - -
- -
- - -
- -
-
-
- - - diff --git a/src/app/modules/colors/pages/explore/explore.component.sass b/src/app/modules/colors/pages/explore/explore.component.sass deleted file mode 100644 index 1775c49..0000000 --- a/src/app/modules/colors/pages/explore/explore.component.sass +++ /dev/null @@ -1,2 +0,0 @@ -.recipe-content > div - margin: 0 3rem 3rem diff --git a/src/app/modules/colors/pages/list/list.component.sass b/src/app/modules/colors/pages/list/list.component.sass deleted file mode 100644 index e5fb34c..0000000 --- a/src/app/modules/colors/pages/list/list.component.sass +++ /dev/null @@ -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) diff --git a/src/app/modules/colors/pages/list/list.component.ts b/src/app/modules/colors/pages/list/list.component.ts deleted file mode 100644 index 637061b..0000000 --- a/src/app/modules/colors/pages/list/list.component.ts +++ /dev/null @@ -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 - } -} diff --git a/src/app/modules/colors/pages/mix/mix-add/mix-add.component.html b/src/app/modules/colors/pages/mix/mix-add/mix-add.component.html deleted file mode 100644 index 128e7d2..0000000 --- a/src/app/modules/colors/pages/mix/mix-add/mix-add.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/src/app/modules/colors/pages/mix/mix-add/mix-add.component.sass b/src/app/modules/colors/pages/mix/mix-add/mix-add.component.sass deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/modules/colors/pages/mix/mix-add/mix-add.component.ts b/src/app/modules/colors/pages/mix/mix-add/mix-add.component.ts deleted file mode 100644 index 0cae5af..0000000 --- a/src/app/modules/colors/pages/mix/mix-add/mix-add.component.ts +++ /dev/null @@ -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}` - ) - } -} diff --git a/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.html b/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.html deleted file mode 100644 index 762bae6..0000000 --- a/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.sass b/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.sass deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.ts b/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.ts deleted file mode 100644 index 5b360fd..0000000 --- a/src/app/modules/colors/pages/mix/mix-edit/mix-edit.component.ts +++ /dev/null @@ -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) - } -} diff --git a/src/app/modules/company/company.module.ts b/src/app/modules/company/company.module.ts index 4d86746..5f9d07b 100644 --- a/src/app/modules/company/company.module.ts +++ b/src/app/modules/company/company.module.ts @@ -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 { } diff --git a/src/app/modules/company/pages/list/list.component.html b/src/app/modules/company/pages/list/list.component.html index e0781de..944d7d2 100644 --- a/src/app/modules/company/pages/list/list.component.html +++ b/src/app/modules/company/pages/list/list.component.html @@ -1,7 +1,26 @@ - - + + + Ajouter + + + + +

Il n'y a actuellement aucune bannière enregistrée dans le système.

+

Vous pouvez en créer une ici.

+
+ + + + Nom + {{company.name}} + + + + + + + Modifier + + + + diff --git a/src/app/modules/company/pages/list/list.component.ts b/src/app/modules/company/pages/list/list.component.ts index 6104358..5899e96 100644 --- a/src/app/modules/company/pages/list/list.component.ts +++ b/src/app/modules/company/pages/list/list.component.ts @@ -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) + } } diff --git a/src/app/modules/company/service/company.service.ts b/src/app/modules/company/service/company.service.ts index 238eaef..90f3a78 100644 --- a/src/app/modules/company/service/company.service.ts +++ b/src/app/modules/company/service/company.service.ts @@ -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' diff --git a/src/app/modules/configuration/bool.html b/src/app/modules/configuration/bool.html deleted file mode 100644 index 48709db..0000000 --- a/src/app/modules/configuration/bool.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - {{lastUpdated}} -
diff --git a/src/app/modules/configuration/config-bool.html b/src/app/modules/configuration/config-bool.html new file mode 100644 index 0000000..0f7a298 --- /dev/null +++ b/src/app/modules/configuration/config-bool.html @@ -0,0 +1,6 @@ + +
+ + {{inputHint}} +
+
diff --git a/src/app/modules/configuration/config-container.html b/src/app/modules/configuration/config-container.html new file mode 100644 index 0000000..8c928f0 --- /dev/null +++ b/src/app/modules/configuration/config-container.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/src/app/modules/configuration/config-date.html b/src/app/modules/configuration/config-date.html new file mode 100644 index 0000000..2f443e3 --- /dev/null +++ b/src/app/modules/configuration/config-date.html @@ -0,0 +1,12 @@ + + + + diff --git a/src/app/modules/configuration/config-editor.html b/src/app/modules/configuration/config-editor.html new file mode 100644 index 0000000..c8af260 --- /dev/null +++ b/src/app/modules/configuration/config-editor.html @@ -0,0 +1,120 @@ +
+ + + Retour + + + Enregistrer + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redémarrer le serveur + + +
+
+ + + + diff --git a/src/app/modules/configuration/config-editor.ts b/src/app/modules/configuration/config-editor.ts new file mode 100644 index 0000000..b12502a --- /dev/null +++ b/src/app/modules/configuration/config-editor.ts @@ -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() + 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) + } +} diff --git a/src/app/modules/configuration/config-image.html b/src/app/modules/configuration/config-image.html new file mode 100644 index 0000000..c801323 --- /dev/null +++ b/src/app/modules/configuration/config-image.html @@ -0,0 +1,28 @@ +
+

+ {{label}} +

+
+ + +
+ + +
+
+ +
+ {{lastUpdated}} +
+
+
diff --git a/src/app/modules/configuration/config-period.html b/src/app/modules/configuration/config-period.html new file mode 100644 index 0000000..90d8af9 --- /dev/null +++ b/src/app/modules/configuration/config-period.html @@ -0,0 +1,7 @@ + + + + diff --git a/src/app/modules/configuration/config-section.html b/src/app/modules/configuration/config-section.html index 08c2b94..8d02e34 100644 --- a/src/app/modules/configuration/config-section.html +++ b/src/app/modules/configuration/config-section.html @@ -1,8 +1,6 @@ - - - + {{label}} diff --git a/src/app/modules/configuration/config-secure.html b/src/app/modules/configuration/config-secure.html new file mode 100644 index 0000000..aaea0b0 --- /dev/null +++ b/src/app/modules/configuration/config-secure.html @@ -0,0 +1,24 @@ + + + {{buttonLabel}} + + + + + + + + + diff --git a/src/app/modules/configuration/config-text.html b/src/app/modules/configuration/config-text.html new file mode 100644 index 0000000..1878b43 --- /dev/null +++ b/src/app/modules/configuration/config-text.html @@ -0,0 +1,11 @@ + + + + diff --git a/src/app/modules/configuration/config.html b/src/app/modules/configuration/config.html deleted file mode 100644 index 8f23ca1..0000000 --- a/src/app/modules/configuration/config.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - -
diff --git a/src/app/modules/configuration/config.module.ts b/src/app/modules/configuration/config.module.ts index 8127ec2..d3d6bba 100644 --- a/src/app/modules/configuration/config.module.ts +++ b/src/app/modules/configuration/config.module.ts @@ -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 { +} diff --git a/src/app/modules/configuration/config.sass b/src/app/modules/configuration/config.sass index 6812169..0e8f839 100644 --- a/src/app/modules/configuration/config.sass +++ b/src/app/modules/configuration/config.sass @@ -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 diff --git a/src/app/modules/configuration/config.ts b/src/app/modules/configuration/config.ts index a40e7de..d40479e 100644 --- a/src/app/modules/configuration/config.ts +++ b/src/app/modules/configuration/config.ts @@ -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() 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() - 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) } } diff --git a/src/app/modules/configuration/date.html b/src/app/modules/configuration/date.html deleted file mode 100644 index 83e8af4..0000000 --- a/src/app/modules/configuration/date.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - -
diff --git a/src/app/modules/configuration/editor.html b/src/app/modules/configuration/editor.html deleted file mode 100644 index 66cd181..0000000 --- a/src/app/modules/configuration/editor.html +++ /dev/null @@ -1,120 +0,0 @@ - - - Retour - - - Enregistrer - - - -
- - Apparence - - - - - - - - - - Logo - - 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'). - - - - - Icône - - 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'). - - - - - - - Données - - - Période d'expiration de l'approbation de l'échantillon des recettes - - - - Période d'expiration des kits de retouches complets - - 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. - - - - - Activer le cache des PDFs générés - - 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. - - - - - - - Système - - - URL de l'instance - - Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques. - - - - - URL de la base de données - - - - Utilisateur de la base de données - - - - Mot de passe de la base de données - - - - Version de la base de données - - - - Version de Color Recipes Explorer - - - - Date de compilation de Color Recipes Explorer - - - - Version de Java - - - - Système d'exploitation - - - - Redémarrer le serveur - - -
- - - - diff --git a/src/app/modules/configuration/image.html b/src/app/modules/configuration/image.html deleted file mode 100644 index 21459eb..0000000 --- a/src/app/modules/configuration/image.html +++ /dev/null @@ -1,26 +0,0 @@ -
-

- -

-
- -
- - -
-
- -
- {{lastUpdated}} -
-
diff --git a/src/app/modules/configuration/period.html b/src/app/modules/configuration/period.html deleted file mode 100644 index 722ec27..0000000 --- a/src/app/modules/configuration/period.html +++ /dev/null @@ -1,7 +0,0 @@ -
- - -
diff --git a/src/app/modules/material-type/material-type.module.ts b/src/app/modules/material-type/material-type.module.ts index 591a8a8..1df904c 100644 --- a/src/app/modules/material-type/material-type.module.ts +++ b/src/app/modules/material-type/material-type.module.ts @@ -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 { } diff --git a/src/app/modules/material-type/pages/list/list.component.html b/src/app/modules/material-type/pages/list/list.component.html index f70251f..6555e6a 100644 --- a/src/app/modules/material-type/pages/list/list.component.html +++ b/src/app/modules/material-type/pages/list/list.component.html @@ -1,7 +1,40 @@ - - + + + Ajouter + + + + +

Il n'y a actuellement aucun type de produit enregistré dans le système.

+

Vous pouvez en créer un ici.

+
+ + + + Nom + {{materialType.name}} + + + + Préfix + {{materialType.prefix}} + + + + Utilise les pourcentages + {{materialType.usePercentages ? 'Oui' : 'Non'}} + + + + + + + Modifier + + + + diff --git a/src/app/modules/material-type/pages/list/list.component.ts b/src/app/modules/material-type/pages/list/list.component.ts index 2556388..c25e58f 100644 --- a/src/app/modules/material-type/pages/list/list.component.ts +++ b/src/app/modules/material-type/pages/list/list.component.ts @@ -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) + } } diff --git a/src/app/modules/material/material.module.ts b/src/app/modules/material/material.module.ts index 6ccb444..782b3ef 100644 --- a/src/app/modules/material/material.module.ts +++ b/src/app/modules/material/material.module.ts @@ -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 { diff --git a/src/app/modules/material/pages/inventory/inventory.component.html b/src/app/modules/material/pages/inventory/inventory.component.html index a964091..130ff57 100644 --- a/src/app/modules/material/pages/inventory/inventory.component.html +++ b/src/app/modules/material/pages/inventory/inventory.component.html @@ -1,62 +1,59 @@ -
- -
- - Recherche par code... - - - - Recherche par type de produit - - - {{materialType.name}} - - - -
- - -
- - Quantité faible - - + + + + + + + + + + + + - -
-
+ + + - +

Il n'y a actuellement aucun produit enregistré dans le système.

+

Vous pouvez en créer un ici. +

+ + + + [filterPredicate]="materialFilterPredicate" + [filter]="filter" + [data]="materials" + [columns]="columns"> - + - + @@ -68,9 +65,7 @@ + - - - - -
CodeCode {{material.name}} Type de produitType de produit {{material.materialType.name}} -
+
-
- - - + + Modifier + - - - + + Fiche signalitique +
+ diff --git a/src/app/modules/material/pages/inventory/inventory.component.sass b/src/app/modules/material/pages/inventory/inventory.component.sass index 20ded20..5867821 100644 --- a/src/app/modules/material/pages/inventory/inventory.component.sass +++ b/src/app/modules/material/pages/inventory/inventory.component.sass @@ -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 diff --git a/src/app/modules/material/pages/inventory/inventory.component.ts b/src/app/modules/material/pages/inventory/inventory.component.ts index aafe764..81f914f 100644 --- a/src/app/modules/material/pages/inventory/inventory.component.ts +++ b/src/app/modules/material/pages/inventory/inventory.component.ts @@ -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 + 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 { - this.dataSource = new MatTableDataSource(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) } diff --git a/src/app/modules/recipes/add.html b/src/app/modules/recipes/add.html new file mode 100644 index 0000000..a4eec0d --- /dev/null +++ b/src/app/modules/recipes/add.html @@ -0,0 +1,10 @@ + + + Retour + + + + + + + diff --git a/src/app/modules/colors/bpac.js b/src/app/modules/recipes/bpac.js similarity index 100% rename from src/app/modules/colors/bpac.js rename to src/app/modules/recipes/bpac.js diff --git a/src/app/modules/colors/components/images-editor/images-editor.component.html b/src/app/modules/recipes/components/images-editor/images-editor.component.html similarity index 89% rename from src/app/modules/colors/components/images-editor/images-editor.component.html rename to src/app/modules/recipes/components/images-editor/images-editor.component.html index e07f7b8..abd0574 100644 --- a/src/app/modules/colors/components/images-editor/images-editor.component.html +++ b/src/app/modules/recipes/components/images-editor/images-editor.component.html @@ -4,6 +4,8 @@
+

Aucune image n'est associée à cette couleur

+
diff --git a/src/app/modules/colors/components/images-editor/images-editor.component.sass b/src/app/modules/recipes/components/images-editor/images-editor.component.sass similarity index 100% rename from src/app/modules/colors/components/images-editor/images-editor.component.sass rename to src/app/modules/recipes/components/images-editor/images-editor.component.sass diff --git a/src/app/modules/colors/components/images-editor/images-editor.component.ts b/src/app/modules/recipes/components/images-editor/images-editor.component.ts similarity index 100% rename from src/app/modules/colors/components/images-editor/images-editor.component.ts rename to src/app/modules/recipes/components/images-editor/images-editor.component.ts diff --git a/src/app/modules/colors/components/mix-table/mix-table.component.html b/src/app/modules/recipes/components/mix-table/mix-table.component.html similarity index 100% rename from src/app/modules/colors/components/mix-table/mix-table.component.html rename to src/app/modules/recipes/components/mix-table/mix-table.component.html diff --git a/src/app/modules/colors/components/mix-table/mix-table.component.sass b/src/app/modules/recipes/components/mix-table/mix-table.component.sass similarity index 90% rename from src/app/modules/colors/components/mix-table/mix-table.component.sass rename to src/app/modules/recipes/components/mix-table/mix-table.component.sass index 927de1b..1f8c7e4 100644 --- a/src/app/modules/colors/components/mix-table/mix-table.component.sass +++ b/src/app/modules/recipes/components/mix-table/mix-table.component.sass @@ -1,4 +1,4 @@ -@import '../../../../../custom-theme' +@import "~src/variables" mat-expansion-panel width: 48rem diff --git a/src/app/modules/colors/components/mix-table/mix-table.component.ts b/src/app/modules/recipes/components/mix-table/mix-table.component.ts similarity index 97% rename from src/app/modules/colors/components/mix-table/mix-table.component.ts rename to src/app/modules/recipes/components/mix-table/mix-table.component.ts index 2018c5a..bd430a6 100644 --- a/src/app/modules/colors/components/mix-table/mix-table.component.ts +++ b/src/app/modules/recipes/components/mix-table/mix-table.component.ts @@ -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 } diff --git a/src/app/modules/colors/components/mixes-card/mixes-card.component.html b/src/app/modules/recipes/components/mixes-card/mixes-card.component.html similarity index 88% rename from src/app/modules/colors/components/mixes-card/mixes-card.component.html rename to src/app/modules/recipes/components/mixes-card/mixes-card.component.html index b6766df..c8a093a 100644 --- a/src/app/modules/colors/components/mixes-card/mixes-card.component.html +++ b/src/app/modules/recipes/components/mixes-card/mixes-card.component.html @@ -3,6 +3,8 @@ Mélanges +

Il n'y a aucun mélange dans cette couleur

+ Étapes - + {{step.position}}.{{step.message}} diff --git a/src/app/modules/colors/components/step-list/step-list.component.sass b/src/app/modules/recipes/components/step-list/step-list.component.sass similarity index 100% rename from src/app/modules/colors/components/step-list/step-list.component.sass rename to src/app/modules/recipes/components/step-list/step-list.component.sass diff --git a/src/app/modules/colors/components/step-list/step-list.component.ts b/src/app/modules/recipes/components/step-list/step-list.component.ts similarity index 100% rename from src/app/modules/colors/components/step-list/step-list.component.ts rename to src/app/modules/recipes/components/step-list/step-list.component.ts diff --git a/src/app/modules/colors/components/step-table/step-table.component.html b/src/app/modules/recipes/components/step-table/step-table.component.html similarity index 100% rename from src/app/modules/colors/components/step-table/step-table.component.html rename to src/app/modules/recipes/components/step-table/step-table.component.html diff --git a/src/app/modules/colors/components/step-table/step-table.component.sass b/src/app/modules/recipes/components/step-table/step-table.component.sass similarity index 100% rename from src/app/modules/colors/components/step-table/step-table.component.sass rename to src/app/modules/recipes/components/step-table/step-table.component.sass diff --git a/src/app/modules/colors/components/step-table/step-table.component.ts b/src/app/modules/recipes/components/step-table/step-table.component.ts similarity index 100% rename from src/app/modules/colors/components/step-table/step-table.component.ts rename to src/app/modules/recipes/components/step-table/step-table.component.ts diff --git a/src/app/modules/colors/components/unit-selector/unit-selector.component.html b/src/app/modules/recipes/components/unit-selector/unit-selector.component.html similarity index 89% rename from src/app/modules/colors/components/unit-selector/unit-selector.component.html rename to src/app/modules/recipes/components/unit-selector/unit-selector.component.html index 4b82721..b00e879 100644 --- a/src/app/modules/colors/components/unit-selector/unit-selector.component.html +++ b/src/app/modules/recipes/components/unit-selector/unit-selector.component.html @@ -1,6 +1,6 @@ Unités - + Millilitres Litres diff --git a/src/app/modules/colors/components/unit-selector/unit-selector.component.sass b/src/app/modules/recipes/components/unit-selector/unit-selector.component.sass similarity index 100% rename from src/app/modules/colors/components/unit-selector/unit-selector.component.sass rename to src/app/modules/recipes/components/unit-selector/unit-selector.component.sass diff --git a/src/app/modules/colors/components/unit-selector/unit-selector.component.ts b/src/app/modules/recipes/components/unit-selector/unit-selector.component.ts similarity index 53% rename from src/app/modules/colors/components/unit-selector/unit-selector.component.ts rename to src/app/modules/recipes/components/unit-selector/unit-selector.component.ts index 418677f..9f8d192 100644 --- a/src/app/modules/colors/components/unit-selector/unit-selector.component.ts +++ b/src/app/modules/recipes/components/unit-selector/unit-selector.component.ts @@ -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() + + ngOnInit() { + this.control?.setValue(this.unit) + } + + onUnitChange(newUnit: string) { + this.control?.setValue(newUnit) + this.unitChange.emit(newUnit) + } } diff --git a/src/app/modules/recipes/edit.html b/src/app/modules/recipes/edit.html new file mode 100644 index 0000000..690dbf6 --- /dev/null +++ b/src/app/modules/recipes/edit.html @@ -0,0 +1,36 @@ +
+ + + Retour + + + + Supprimer + Enregistrer + + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + diff --git a/src/app/modules/recipes/explore.html b/src/app/modules/recipes/explore.html new file mode 100644 index 0000000..8d8f714 --- /dev/null +++ b/src/app/modules/recipes/explore.html @@ -0,0 +1,52 @@ +
+ + + + + + Retour + + + + + + + + + Version Excel + + Enregistrer + + + + +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+
+
+ + + diff --git a/src/app/modules/colors/pages/explore/explore.component.ts b/src/app/modules/recipes/explore.ts similarity index 60% rename from src/app/modules/colors/pages/explore/explore.component.ts rename to src/app/modules/recipes/explore.ts index 4fef579..084693e 100644 --- a/src/app/modules/colors/pages/explore/explore.component.ts +++ b/src/app/modules/recipes/explore.ts @@ -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() 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) { 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 diff --git a/src/app/modules/recipes/form.html b/src/app/modules/recipes/form.html new file mode 100644 index 0000000..3dc9805 --- /dev/null +++ b/src/app/modules/recipes/form.html @@ -0,0 +1,21 @@ +
+ +

Il n'y a actuellement aucune bannière enregistrée dans le système.

+

Vous pouvez en créer une ici.

+
+
+ + + Ajouter une couleur + Modifier la couleur {{recipe.name}} + + + + + + + + + + + diff --git a/src/app/modules/colors/pages/list/list.component.html b/src/app/modules/recipes/list.html similarity index 57% rename from src/app/modules/colors/pages/list/list.component.html rename to src/app/modules/recipes/list.html index 84493bf..37ed007 100644 --- a/src/app/modules/colors/pages/list/list.component.html +++ b/src/app/modules/recipes/list.html @@ -1,43 +1,47 @@ -
- - Recherche - - - -
- -
+ + + + + + Ajouter + + + +
+ +

Il n'y a actuellement aucune bannière enregistrée dans le système.

+

Vous pouvez en créer une ici. +

+
+ +

Il n'y a actuellement aucune recette enregistrée dans le système.

+

Vous pouvez en créer une ici.

+
- {{companyRecipes.company}} + {{company.name}} - + - + @@ -85,19 +89,16 @@ - - - - - -
Nom - + + Voir - + + Modifier
+
diff --git a/src/app/modules/recipes/list.ts b/src/app/modules/recipes/list.ts new file mode 100644 index 0000000..121f6e7 --- /dev/null +++ b/src/app/modules/recipes/list.ts @@ -0,0 +1,110 @@ +import {ChangeDetectorRef, Component} from '@angular/core' +import {ErrorHandlingComponent} from '../shared/components/subscribing.component' +import {Company} from '../shared/model/company.model' +import {getRecipeLuma, Recipe, recipeMatchesFilter} from '../shared/model/recipe.model' +import {CompanyService} from '../company/service/company.service' +import {RecipeService} from './services/recipe.service' +import {AccountService} from '../accounts/services/account.service' +import {ConfigService} from '../shared/service/config.service' +import {AppState} from '../shared/app-state' +import {ErrorService} from '../shared/service/error.service' +import {ActivatedRoute, Router} from '@angular/router' +import {Config} from '../shared/model/config.model' +import {Permission} from '../shared/model/user' +import {FormControl} from '@angular/forms' + +@Component({ + selector: 'cre-recipe-list', + templateUrl: 'list.html', + styleUrls: ['recipes.sass'] +}) +export class RecipeList extends ErrorHandlingComponent { + companies: Company[] = [] + recipes: Map = new Map() + columns = ['name', 'description', 'color', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit'] + panelForcedExpanded = false + + searchControl: FormControl + searchQuery = '' + + recipeFilterPredicate = recipeMatchesFilter + + constructor( + private companyService: CompanyService, + private recipeService: RecipeService, + private accountService: AccountService, + private configService: ConfigService, + private cdRef: ChangeDetectorRef, + private appState: AppState, + errorService: ErrorService, + router: Router, + activatedRoute: ActivatedRoute + ) { + super(errorService, activatedRoute, router) + } + + ngOnInit() { + super.ngOnInit() + this.appState.title = 'Explorateur' + + // Navigate to configs if server is in emergency mode + this.subscribe( + this.configService.get(Config.EMERGENCY_MODE), + config => { + if (config.content == 'true') { + this.urlUtils.navigateTo('/admin/config/') + } + } + ) + + this.fetchCompanies() + this.fetchRecipes() + + this.searchControl = new FormControl('') + this.subscribe( + this.searchControl.valueChanges, + value => { + this.searchQuery = value + if (value.length > 0 && !this.panelForcedExpanded) { + this.panelForcedExpanded = true + this.cdRef.detectChanges() + } + } + ) + } + + private fetchCompanies() { + this.subscribe( + this.companyService.all, + companies => this.companies = companies + ) + } + + private fetchRecipes() { + this.subscribe( + this.recipeService.allByCompany, + recipes => this.recipes = recipes, + true + ) + } + + isCompanyHidden(company: Company): boolean { + const companyRecipes = this.recipes.get(company.id) + return !(companyRecipes && companyRecipes.length >= 0) || + this.searchQuery && this.searchQuery.length > 0 && + !companyRecipes.some(recipe => this.recipeFilterPredicate(recipe, this.searchQuery)) + } + + + isLight(recipe: Recipe): boolean { + return getRecipeLuma(recipe) > 200 + } + + get hasEditPermission(): boolean { + return this.accountService.hasPermission(Permission.EDIT_RECIPES) + } + + get hasCompanyEditPermission(): boolean { + return this.accountService.hasPermission(Permission.EDIT_COMPANIES) + } +} diff --git a/src/app/modules/recipes/mix/add.html b/src/app/modules/recipes/mix/add.html new file mode 100644 index 0000000..d6a6301 --- /dev/null +++ b/src/app/modules/recipes/mix/add.html @@ -0,0 +1,20 @@ + + + + Retour + + + Enregistrer + + + + + + Ajouter un mélange à la couleur {{recipe.company.name}} - {{recipe.name}} + + + diff --git a/src/app/modules/recipes/mix/edit.html b/src/app/modules/recipes/mix/edit.html new file mode 100644 index 0000000..14267ca --- /dev/null +++ b/src/app/modules/recipes/mix/edit.html @@ -0,0 +1,25 @@ + + + + Retour + + + + Enregistrer + + + + + + + Modification du mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}} + + + diff --git a/src/app/modules/recipes/mix/form.html b/src/app/modules/recipes/mix/form.html new file mode 100644 index 0000000..8deb6d4 --- /dev/null +++ b/src/app/modules/recipes/mix/form.html @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/src/app/modules/recipes/mix/info-form.html b/src/app/modules/recipes/mix/info-form.html new file mode 100644 index 0000000..adb993d --- /dev/null +++ b/src/app/modules/recipes/mix/info-form.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/app/modules/recipes/mix/materials-form-combo-box.html b/src/app/modules/recipes/mix/materials-form-combo-box.html new file mode 100644 index 0000000..fc440c9 --- /dev/null +++ b/src/app/modules/recipes/mix/materials-form-combo-box.html @@ -0,0 +1,5 @@ + + diff --git a/src/app/modules/recipes/mix/materials-form.html b/src/app/modules/recipes/mix/materials-form.html new file mode 100644 index 0000000..5c7742c --- /dev/null +++ b/src/app/modules/recipes/mix/materials-form.html @@ -0,0 +1,76 @@ +
+ +

Il n'y a actuellement aucun produit enregistré dans le système.

+

Vous pouvez en créer un ici. +

+
+ + + + Position + {{mixMaterial.position}} + + + + + + + + + + + + Produit + + + + + + + + Quantité + + + + + + + Unités + + + + + + % + + + + + + + Ajouter + + + + Retirer + + + + +
diff --git a/src/app/modules/recipes/mix/materials-form.ts b/src/app/modules/recipes/mix/materials-form.ts new file mode 100644 index 0000000..06b22d7 --- /dev/null +++ b/src/app/modules/recipes/mix/materials-form.ts @@ -0,0 +1,255 @@ +import {AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild, ViewChildren} from '@angular/core' +import {CreTable} from '../../shared/components/tables/tables' +import {Mix, MixMaterialDto, mixMaterialsToMixMaterialsDto, sortMixMaterialsDto} from '../../shared/model/recipe.model' +import {Observable, Subject} from 'rxjs' +import {Material, materialComparator} from '../../shared/model/material.model' +import {FormControl, Validators} from '@angular/forms' +import {takeUntil} from 'rxjs/operators' +import {CreComboBoxComponent, CreInputEntry} from '../../shared/components/inputs/inputs' +import {AccountService} from '../../accounts/services/account.service' +import {Permission} from '../../shared/model/user' +import {UNIT_MILLILITER} from '../../shared/units' + +@Component({ + selector: 'cre-mix-materials-form-combo-box', + templateUrl: 'materials-form-combo-box.html' +}) +export class MixMaterialsFormComboBox implements OnInit { + @ViewChild(CreComboBoxComponent) comboBox: CreComboBoxComponent + + @Input() mixMaterial: MixMaterialDto + @Input() mix: Mix | null + @Input() mixMaterials: MixMaterialDto[] + @Input() control: FormControl + @Input() materials: Material[] + @Input() position: number + + entries: CreInputEntry[] + + ngOnInit() { + this.entries = this.filterMaterials() + } + + updateEntries() { + this.entries = this.filterMaterials() + this.comboBox.reloadEntries() + } + + private filterMaterials(): CreInputEntry[] { + return this.materials + .filter(material => { + if (this.mix && this.mix.mixType.material.id === material.id) { + return false + } + + // Prevent use of percents in first position + if (material.materialType.usePercentages && this.mixMaterial.position <= 1) { + return false + } + + if (this.mixMaterial.materialId === material.id) { + return true + } + + return this.mixMaterials.filter(x => x.materialId === material.id).length <= 0 + }) + .sort(materialComparator) + .map(material => new CreInputEntry(material.id, material.name, material.materialType.prefix ? `[${material.materialType.prefix}] ${material.name}` : material.name)) + } +} + +@Component({ + selector: 'cre-mix-materials-form', + templateUrl: 'materials-form.html' +}) +export class MixMaterialsForm implements AfterViewInit, OnDestroy { + @ViewChild(CreTable) table: CreTable + @ViewChildren(MixMaterialsFormComboBox) comboBoxes: MixMaterialsFormComboBox[] + + @Input() materials: Observable + @Input() mix: Mix | null + + mixMaterials: MixMaterialDto[] = [] + columns = ['position', 'positionButtons', 'material', 'quantity', 'units', 'endButton'] + allMaterials: Material[] + + private _controls: ControlsByPosition[] = [] + private _destroy$ = new Subject() + + constructor( + private accountService: AccountService, + private cdRef: ChangeDetectorRef + ) { + } + + ngAfterViewInit() { + this.materials.subscribe({ + next: materials => { + this.allMaterials = materials + + if (!this.mix) { + this.addRow() + } else { + mixMaterialsToMixMaterialsDto(this.mix).forEach(x => this.insertRow(x)) + } + + this.table.renderRows() + this.cdRef.detectChanges() + } + }) + } + + ngOnDestroy() { + this._destroy$.next(true) + this._destroy$.complete() + } + + addRow() { + const mixMaterial = new MixMaterialDto(null, 0, false, this.nextPosition, UNIT_MILLILITER) + this.insertRow(mixMaterial) + this.table.renderRows() + } + + insertRow(mixMaterial: MixMaterialDto) { + const materialIdControl = new FormControl(mixMaterial.materialId, Validators.required) + const quantityControl = new FormControl(mixMaterial.quantity, Validators.required) + const unitsControl = new FormControl(mixMaterial.units, Validators.required) + + materialIdControl.valueChanges + .pipe(takeUntil(this._destroy$)) + .subscribe({ + next: materialId => { + mixMaterial.materialId = materialId + this.refreshAvailableMaterials() + this.cdRef.detectChanges() + } + }) + + this.mixMaterials.push(mixMaterial) + this._controls.push({ + position: mixMaterial.position, + controls: { + materialId: materialIdControl, + quantity: quantityControl, + units: unitsControl + } + }) + } + + removeRow(mixMaterial: MixMaterialDto) { + this.mixMaterials = this.mixMaterials.filter(x => x.position !== mixMaterial.position) + this._controls = this._controls.filter(x => x.position !== mixMaterial.position) + + for (let position = mixMaterial.position + 1; position < this.mixMaterials.length; position++) { + this.updatePosition(this.getMixMaterialByPosition(position), position - 1, false) + } + } + + updatePosition(mixMaterial: MixMaterialDto, newPosition: number, switchPositions = true) { + const currentPosition = mixMaterial.position + const currentControls = this.getControlsByPosition(currentPosition) + + // Update before current to prevent position conflicts + if (switchPositions) { + this.updatePosition(this.getMixMaterialByPosition(newPosition), currentPosition, false) + } + + mixMaterial.position = newPosition + currentControls.position = newPosition + + this.sortTable() + this.refreshAvailableMaterials() + } + + getControls(position: number): MixMaterialControls { + return this.getControlsByPosition(position).controls + } + + areUnitsPercents(mixMaterial: MixMaterialDto): boolean { + if (!mixMaterial) { + return false + } + + return mixMaterial.materialId ? this.allMaterials?.filter(x => x.id === mixMaterial.materialId)[0].materialType.usePercentages : false + } + + isDecreasePositionButtonDisabled(mixMaterial: MixMaterialDto): boolean { + return mixMaterial.position <= 2 && this.areUnitsPercents(mixMaterial) + } + + isIncreasePositionButtonDisabled(mixMaterial: MixMaterialDto): boolean { + if (mixMaterial.position === this.mixMaterials.length) { + return true + } + + if (mixMaterial.position > 1) { + return false + } + + const nextMixMaterial = this.getMixMaterialByPosition(mixMaterial.position + 1) + return this.areUnitsPercents(nextMixMaterial) + } + + get hasMaterialEditPermission(): boolean { + return this.accountService.hasPermission(Permission.EDIT_MATERIALS) + } + + get materialCount(): number { + return this.allMaterials ? this.allMaterials.length : 0 + } + + get updatedMixMaterials(): MixMaterialDto[] { + const updatedMixMaterials: MixMaterialDto[] = [] + this.mixMaterials.forEach(mixMaterial => { + const controls = this.getControlsByPosition(mixMaterial.position).controls + updatedMixMaterials.push({ + materialId: controls.materialId.value, + quantity: controls.quantity.value, + position: mixMaterial.position, + units: controls.units.value, + isPercents: this.areUnitsPercents(mixMaterial) + }) + }) + return updatedMixMaterials + } + + get valid(): boolean { + return this._controls + .map(controls => controls.controls) + .map(controls => [controls.materialId, controls.quantity]) + .flatMap(controls => controls) + .every(control => control.valid) + } + + private get nextPosition(): number { + return this.mixMaterials.length + 1 + } + + private getMixMaterialByPosition(position: number): MixMaterialDto { + return this.mixMaterials.filter(x => x.position === position)[0] + } + + private getControlsByPosition(position: number): ControlsByPosition { + return this._controls.filter(control => control.position === position)[0] + } + + private refreshAvailableMaterials() { + this.comboBoxes.forEach(x => x.updateEntries()) + } + + private sortTable() { + this.mixMaterials = sortMixMaterialsDto(this.mixMaterials) + this.table.renderRows() + } +} + +interface MixMaterialControls { + materialId: FormControl + quantity: FormControl + units: FormControl +} + +interface ControlsByPosition { + position: number + controls: MixMaterialControls +} diff --git a/src/app/modules/recipes/mix/mix.ts b/src/app/modules/recipes/mix/mix.ts new file mode 100644 index 0000000..a91833f --- /dev/null +++ b/src/app/modules/recipes/mix/mix.ts @@ -0,0 +1,171 @@ +import {Component, Directive, Input, OnInit, ViewChild} from '@angular/core' +import {SubscribingComponent} from '../../shared/components/subscribing.component' +import {Mix, Recipe} from '../../shared/model/recipe.model' +import {ErrorService} from '../../shared/service/error.service' +import {ActivatedRoute, Router} from '@angular/router' +import {RecipeService} from '../services/recipe.service' +import {FormControl, Validators} from '@angular/forms' +import {Observable} from 'rxjs' +import {MaterialType} from '../../shared/model/materialtype.model' +import {MaterialTypeService} from '../../material-type/service/material-type.service' +import {CreInputEntry} from '../../shared/components/inputs/inputs' +import {map} from 'rxjs/operators' +import {Material} from '../../shared/model/material.model' +import {MaterialService} from '../../material/service/material.service' +import {CreForm} from '../../shared/components/forms/forms' +import {MixMaterialsForm} from './materials-form' +import {MixSaveDto, MixService, MixUpdateDto} from '../services/mix.service' + +@Directive() +abstract class _BaseMixPage extends SubscribingComponent { + materialTypes$ = this.materialTypeService.all + materials$: Observable + + private _recipe: Recipe | null + + constructor( + protected mixService: MixService, + private recipeService: RecipeService, + private materialTypeService: MaterialTypeService, + private materialService: MaterialService, + errorService: ErrorService, + router: Router, + activatedRoute: ActivatedRoute + ) { + super(errorService, activatedRoute, router) + } + + ngOnInit() { + this.fetchRecipe() + } + + private fetchRecipe() { + const recipeId = this.urlUtils.parseIntUrlParam('recipeId') + + this.subscribe( + this.recipeService.getById(recipeId), + recipe => this.recipe = recipe + ) + } + + set recipe(recipe: Recipe) { + this._recipe = recipe + this.materials$ = this.materialService.getAllForMixCreation(recipe.id) + } + + get recipe(): Recipe { + return this._recipe + } + + abstract submit(dto: MixSaveDto) +} + +@Component({ + selector: 'cre-mix-add', + templateUrl: 'add.html' +}) +export class MixAdd extends _BaseMixPage { + submit(dto: MixSaveDto) { + this.subscribeAndNavigate( + this.mixService.saveDto(dto), + `/color/edit/${this.recipe.id}` + ) + } +} + +@Component({ + selector: 'cre-mix-edit', + templateUrl: 'edit.html' +}) +export class MixEdit extends _BaseMixPage { + mix: Mix + + ngOnInit() { + super.ngOnInit() + + this.fetchMix() + } + + private fetchMix() { + const mixId = this.urlUtils.parseIntUrlParam('id') + + this.subscribe( + this.mixService.getById(mixId), + mix => this.mix = mix + ) + } + + submit(dto: MixSaveDto) { + this.subscribeAndNavigate( + this.mixService.updateDto({...dto, id: this.mix.id}), + `/color/edit/${this.recipe.id}` + ) + } +} + +@Component({ + selector: 'cre-mix-info-form', + templateUrl: 'info-form.html' +}) +export class MixInfoForm implements OnInit { + @ViewChild(CreForm) form: CreForm + + @Input() recipe: Recipe + @Input() mix: Mix | null + @Input() materialTypes: Observable + + materialTypeEntries: Observable + controls: any + + ngOnInit() { + this.materialTypeEntries = this.materialTypes.pipe( + map(materialTypes => { + return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name)) + }) + ) + + this.controls = { + name: new FormControl(this.mix?.mixType.name, Validators.required), + materialType: new FormControl(this.mix?.mixType.material.materialType.id, Validators.required) + } + } + + get mixName(): string { + return this.controls.name.value + } + + get mixMaterialTypeId(): number { + return this.controls.materialType.value + } + + get valid(): boolean { + return this.form.valid + } +} + +@Component({ + selector: 'cre-mix-form', + templateUrl: 'form.html' +}) +export class MixForm { + @ViewChild(MixInfoForm) infoForm: MixInfoForm + @ViewChild(MixMaterialsForm) mixMaterialsForm: MixMaterialsForm + + @Input() recipe: Recipe + @Input() mix: Mix | null + @Input() materialTypes: Observable + @Input() materials: Observable + + get formValues(): MixSaveDto { + return { + name: this.infoForm.mixName, + recipeId: this.recipe.id, + materialTypeId: this.infoForm.mixMaterialTypeId, + mixMaterials: this.mixMaterialsForm.updatedMixMaterials + } + } + + get valid(): boolean { + return this.infoForm?.valid && this.mixMaterialsForm?.valid + } +} diff --git a/src/app/modules/colors/ptouchPrint.js b/src/app/modules/recipes/ptouchPrint.js similarity index 100% rename from src/app/modules/colors/ptouchPrint.js rename to src/app/modules/recipes/ptouchPrint.js diff --git a/src/app/modules/recipes/recipes-routing.module.ts b/src/app/modules/recipes/recipes-routing.module.ts new file mode 100644 index 0000000..dca257e --- /dev/null +++ b/src/app/modules/recipes/recipes-routing.module.ts @@ -0,0 +1,37 @@ +import {NgModule} from '@angular/core' +import {RouterModule, Routes} from '@angular/router' +import {CreRecipeExplore} from './explore' +import {RecipeAdd, RecipeEdit} from './recipes' +import {RecipeList} from './list' +import {MixAdd, MixEdit} from './mix/mix' + +const routes: Routes = [{ + path: 'list', + component: RecipeList +}, { + path: 'add', + component: RecipeAdd +}, { + path: 'edit/:id', + component: RecipeEdit +}, { + path: 'add/mix/:recipeId', + component: MixAdd +}, { + path: 'edit/mix/:recipeId/:id', + component: MixEdit +}, { + path: 'explore/:id', + component: CreRecipeExplore +}, { + path: '', + pathMatch: 'full', + redirectTo: 'list' +}] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class RecipesRoutingModule { +} diff --git a/src/app/modules/recipes/recipes.module.ts b/src/app/modules/recipes/recipes.module.ts new file mode 100644 index 0000000..d1787b2 --- /dev/null +++ b/src/app/modules/recipes/recipes.module.ts @@ -0,0 +1,62 @@ +import {NgModule} from '@angular/core' + +import {RecipesRoutingModule} from './recipes-routing.module' +import {SharedModule} from '../shared/shared.module' +import {MatExpansionModule} from '@angular/material/expansion' +import {FormsModule} from '@angular/forms' +import {CreRecipeExplore} from './explore' +import {RecipeInfoComponent} from './components/recipe-info/recipe-info.component' +import {MixTableComponent} from './components/mix-table/mix-table.component' +import {StepListComponent} from './components/step-list/step-list.component' +import {StepTableComponent} from './components/step-table/step-table.component' +import {UnitSelectorComponent} from './components/unit-selector/unit-selector.component' +import {ImagesEditorComponent} from './components/images-editor/images-editor.component' +import {MixesCardComponent} from './components/mixes-card/mixes-card.component' +import {MatSortModule} from '@angular/material/sort' +import {CreInputsModule} from '../shared/components/inputs/inputs.module' +import {CreButtonsModule} from '../shared/components/buttons/buttons.module' +import {RecipeAdd, RecipeEdit, RecipeForm} from './recipes' +import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module' +import {RecipeList} from './list' +import {MixAdd, MixEdit, MixForm, MixInfoForm} from './mix/mix' +import {CreTablesModule} from '../shared/components/tables/tables.module' +import {MixMaterialsForm, MixMaterialsFormComboBox} from './mix/materials-form' + +@NgModule({ + declarations: [ + CreRecipeExplore, + RecipeInfoComponent, + MixTableComponent, + StepListComponent, + StepTableComponent, + UnitSelectorComponent, + ImagesEditorComponent, + MixesCardComponent, + RecipeForm, + RecipeAdd, + RecipeEdit, + RecipeList, + MixAdd, + MixEdit, + MixForm, + MixInfoForm, + MixMaterialsForm, + MixMaterialsFormComboBox + ], + exports: [ + UnitSelectorComponent + ], + imports: [ + RecipesRoutingModule, + SharedModule, + MatExpansionModule, + FormsModule, + MatSortModule, + CreInputsModule, + CreButtonsModule, + CreActionBarModule, + CreTablesModule + ] +}) +export class RecipesModule { +} diff --git a/src/app/modules/recipes/recipes.sass b/src/app/modules/recipes/recipes.sass new file mode 100644 index 0000000..44c8af6 --- /dev/null +++ b/src/app/modules/recipes/recipes.sass @@ -0,0 +1,30 @@ +.recipe-wrapper > section + margin: 0 3rem 3rem + +cre-form + margin-top: 0 !important + +mat-expansion-panel + width: 60rem + margin: 20px auto + +.button-add + margin-top: .8rem + +.recipe-color-circle + box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12) + +.recipe-content > div + margin: 0 3rem 3rem + +cre-table + .mat-column-name, + .mat-column-color, + .mat-column-iconNotApproved + width: 5em + + .mat-column-description + width: 50em + + .mat-column-sample + width: 10em diff --git a/src/app/modules/recipes/recipes.ts b/src/app/modules/recipes/recipes.ts new file mode 100644 index 0000000..e1b6410 --- /dev/null +++ b/src/app/modules/recipes/recipes.ts @@ -0,0 +1,211 @@ +import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'; +import {Observable, Subject} from 'rxjs'; +import {CreInputEntry} from '../shared/components/inputs/inputs'; +import {map, tap} from 'rxjs/operators'; +import {RecipeService} from './services/recipe.service'; +import {CompanyService} from '../company/service/company.service'; +import {AppState} from '../shared/app-state'; +import {ErrorHandler, ErrorService} from '../shared/service/error.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {FormControl, Validators} from '@angular/forms'; +import {Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core'; +import {Recipe, recipeMixCount, RecipeStep, recipeStepCount} from '../shared/model/recipe.model'; +import {AccountService} from '../accounts/services/account.service'; +import {Permission} from '../shared/model/user'; +import {AlertService} from '../shared/service/alert.service'; +import {GroupService} from '../groups/services/group.service'; +import {StepTableComponent} from './components/step-table/step-table.component'; +import {anyMap} from '../shared/utils/map.utils'; +import {CreForm, ICreForm} from '../shared/components/forms/forms'; + +@Component({ + selector: 'recipe-form', + templateUrl: 'form.html', + styleUrls: ['recipes.sass'], + encapsulation: ViewEncapsulation.None +}) +export class RecipeForm extends SubscribingComponent { + @ViewChild(CreForm) creForm: ICreForm + + @Input() recipe: Recipe | null + + @Output() submitForm = new EventEmitter(); + + controls: any + companyEntries$: Observable + hasCompanies = true + + constructor( + private companyService: CompanyService, + private accountService: AccountService, + errorService: ErrorService, + activatedRoute: ActivatedRoute, + router: Router, + ) { + super(errorService, activatedRoute, router) + } + + ngOnInit() { + super.ngOnInit() + + this.fetchCompanies() + + this.controls = { + name: new FormControl(this.recipe?.name, Validators.required), + description: new FormControl(this.recipe?.description, Validators.required), + color: new FormControl(this.recipe?.color ?? '#ffffff', Validators.required), + gloss: new FormControl(this.recipe?.gloss ?? 0, Validators.compose([Validators.required, Validators.min(0), Validators.max(100)])), + sample: new FormControl(this.recipe?.sample, Validators.compose([Validators.required, Validators.min(0)])), + approbationDate: new FormControl(this.recipe?.approbationDate), + remark: new FormControl(this.recipe?.remark), + company: new FormControl({value: this.recipe?.company.id, disabled: !!this.recipe}, Validators.required) + } + } + + private fetchCompanies() { + this.companyEntries$ = this.companyService.all.pipe( + tap(companies => this.hasCompanies = companies.length > 0), + map(companies => companies.map(c => new CreInputEntry(c.id, c.name))), + ) + } + + submit() { + this.submitForm.emit(this.updatedRecipe) + } + + get updatedRecipe(): Recipe { + return { + ...this.recipe, + name: this.controls.name.value, + description: this.controls.description.value, + color: this.controls.color.value, + gloss: this.controls.gloss.value, + sample: this.controls.sample.value, + approbationDate: this.controls.approbationDate.value, + remark: this.controls.remark.value, + company: this.controls.company.value, + } + } + + get hasCompanyEditPermission(): boolean { + return this.accountService.hasPermission(Permission.EDIT_COMPANIES) + } +} + +@Component({ + selector: 'cre-recipe-add', + templateUrl: 'add.html' +}) +export class RecipeAdd extends ErrorHandlingComponent { + errorHandlers = [{ + filter: error => error.type === `exists-recipe-company-name`, + messageProducer: error => `Une couleur avec le nom ${error.name} existe déjà pour la bannière ${error.company}` + }] + + constructor( + private recipeService: RecipeService, + private companyService: CompanyService, + private appState: AppState, + errorService: ErrorService, + router: Router, + activatedRoute: ActivatedRoute + ) { + super(errorService, activatedRoute, router) + this.appState.title = 'Nouvelle couleur' + } + + submit(recipe: Recipe) { + this.subscribe( + this.recipeService.save(recipe), + recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`) + ) + } +} + +@Component({ + selector: 'cre-recipe-edit', + templateUrl: 'edit.html', + styleUrls: ['recipes.sass'] +}) +export class RecipeEdit extends ErrorHandlingComponent { + @ViewChild(StepTableComponent) stepTable: StepTableComponent + @ViewChild(RecipeForm) form: RecipeForm + + recipe: Recipe + groups$ = this.groupService.all + units$ = new Subject() + + errorHandlers: ErrorHandler[] = [{ + filter: error => error.type === 'notfound-recipe-id', + consumer: _ => this.urlUtils.navigateTo('/color/list') + }] + + constructor( + private recipeService: RecipeService, + private companyService: CompanyService, + private groupService: GroupService, + private appState: AppState, + private alertService: AlertService, + errorService: ErrorService, + router: Router, + activatedRoute: ActivatedRoute + ) { + super(errorService, activatedRoute, router) + + this.fetchRecipe() + } + + private fetchRecipe() { + const recipeId = this.urlUtils.parseIntUrlParam('id') + this.subscribe( + this.recipeService.getById(recipeId), + recipe => { + this.recipe = recipe + this.appState.title = `${recipe.name} (Modifications)` + + if (recipeMixCount(this.recipe) == 0) { + this.alertService.pushWarning('Il n\'y a aucun mélange dans cette recette') + } + if (recipeStepCount(this.recipe) == 0) { + this.alertService.pushWarning('Il n\'y a aucune étape dans cette recette') + } + }, + true, + 1 + ) + } + + changeUnits(unit: string) { + this.units$.next(unit) + } + + submit() { + const recipe = this.form.updatedRecipe + const steps = this.stepTable.mappedUpdatedSteps + + if (!this.stepsPositionsAreValid(steps)) { + this.alertService.pushError('Les étapes ne peuvent pas avoir une position inférieure à 1') + return + } + + this.subscribeAndNavigate( + this.recipeService.update(recipe, steps), + '/color/list' + ) + } + + delete() { + this.subscribeAndNavigate( + this.recipeService.delete(this.recipe.id), + '/color/list' + ) + } + + get loggedInUserGroupId(): number { + return this.appState.authenticatedUser.group?.id + } + + private stepsPositionsAreValid(steps: Map): boolean { + return !anyMap(steps, (groupId, steps) => !!steps.find(s => s.position === 0)) + } +} diff --git a/src/app/modules/colors/services/mix.service.ts b/src/app/modules/recipes/services/mix.service.ts similarity index 66% rename from src/app/modules/colors/services/mix.service.ts rename to src/app/modules/recipes/services/mix.service.ts index cfd57bf..6a523e1 100644 --- a/src/app/modules/colors/services/mix.service.ts +++ b/src/app/modules/recipes/services/mix.service.ts @@ -21,8 +21,17 @@ export class MixService { return this.api.get(`/recipe/mix/${id}`) } - saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable { - return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units)) + saveDto(dto: MixSaveDto): Observable { + return this.saveWithUnits( + dto.name, + dto.recipeId, + dto.materialTypeId, + dto.mixMaterials, + ) + } + + saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable { + return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials)) } save(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable { @@ -36,8 +45,17 @@ export class MixService { return this.api.post('/recipe/mix', body) } - updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable { - return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units)) + updateDto(dto: MixUpdateDto): Observable { + return this.updateWithUnits( + dto.id, + dto.name, + dto.materialTypeId, + dto.mixMaterials + ) + } + + updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable { + return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials)) } update(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable { @@ -56,11 +74,12 @@ export class MixService { return this.api.delete(`/recipe/mix/${id}`) } - private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[], units: string): MixMaterialDto[] { - return mixMaterials.map(m => { - m.quantity = convertMixMaterialQuantity(m, units, UNIT_MILLILITER) - return m - }) + private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[]): MixMaterialDto[] { + return mixMaterials.map(mixMaterial => ({ + ...mixMaterial, + quantity: convertMixMaterialQuantity(mixMaterial, UNIT_MILLILITER), + units: UNIT_MILLILITER + })) } private appendMixMaterialsToBody(mixMaterials: MixMaterialDto[], body: any) { @@ -74,3 +93,17 @@ export class MixService { } } +export interface MixSaveDto { + name: string + recipeId: number + materialTypeId: number + mixMaterials: MixMaterialDto[] +} + +export interface MixUpdateDto { + id: number + name: string + materialTypeId: number + mixMaterials: MixMaterialDto[] +} + diff --git a/src/app/modules/colors/services/recipe-image.service.ts b/src/app/modules/recipes/services/recipe-image.service.ts similarity index 100% rename from src/app/modules/colors/services/recipe-image.service.ts rename to src/app/modules/recipes/services/recipe-image.service.ts diff --git a/src/app/modules/colors/services/recipe.service.ts b/src/app/modules/recipes/services/recipe.service.ts similarity index 61% rename from src/app/modules/colors/services/recipe.service.ts rename to src/app/modules/recipes/services/recipe.service.ts index 1cda0ce..4554569 100644 --- a/src/app/modules/colors/services/recipe.service.ts +++ b/src/app/modules/recipes/services/recipe.service.ts @@ -21,16 +21,16 @@ export class RecipeService { return this.api.get(`/recipe?name=${name}`) } - get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> { + get allByCompany(): Observable> { return this.all.pipe(map(recipes => { - const mapped = [] + const map = new Map() recipes.forEach(r => { - if (!mapped[r.company.id]) { - mapped[r.company.id] = {company: r.company.name, recipes: []} + if (!map.has(r.company.id)) { + map.set(r.company.id, []) } - mapped[r.company.id].recipes.push(r) + map.get(r.company.id).push(r) }) - return mapped.filter(e => e != null) // Filter to remove empty elements in the array that appears for some reason + return map })) } @@ -38,20 +38,19 @@ export class RecipeService { return this.api.get(`/recipe/${id}`) } - save(name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, companyId: number): Observable { - const body = {name, description, color, gloss, sample, remark, companyId} - if (approbationDate) { - // @ts-ignore - body.approbationDate = approbationDate + save(recipe: Recipe): Observable { + const body = { + ...recipe, + companyId: recipe.company } + return this.api.post('/recipe', body) } - update(id: number, name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, steps: Map) { - const body = {id, name, description, color, gloss, sample, remark, steps: []} - if (approbationDate) { - // @ts-ignore - body.approbationDate = approbationDate + update(recipe: Recipe, steps: Map) { + const body = { + ...recipe, + steps: [] } steps.forEach((groupSteps, groupId) => { diff --git a/src/app/modules/shared/app-state.ts b/src/app/modules/shared/app-state.ts index f850b3e..5b1c93e 100644 --- a/src/app/modules/shared/app-state.ts +++ b/src/app/modules/shared/app-state.ts @@ -8,7 +8,7 @@ import {Title} from '@angular/platform-browser' }) export class AppState { private readonly KEY_AUTHENTICATED = 'authenticated' - private readonly KEY_AUTHENTICATION_EXPIRATION = 'authentication-expiration' + private readonly KEY_DEFAULT_GROUP_USER_AUTHENTICATED = 'default-group-user-authenticated' private readonly KEY_LOGGED_IN_USER = 'logged-in-user' authenticatedUser$ = new Subject<{ authenticated: boolean, authenticatedUser: User }>() @@ -19,9 +19,19 @@ export class AppState { ) { } + authenticateUser(user: User) { + this.authenticatedUser = user + this.isAuthenticated = true + } + + authenticateGroupUser(user: User) { + this.authenticatedUser = user + this.isDefaultGroupUserAuthenticated = true + } + resetAuthenticatedUser() { this.isAuthenticated = false - this.authenticationExpiration = -1 + this.isDefaultGroupUserAuthenticated = false this.authenticatedUser = null } @@ -36,7 +46,7 @@ export class AppState { return sessionStorage.getItem(this.KEY_AUTHENTICATED) === 'true' } - set isAuthenticated(value: boolean) { + private set isAuthenticated(value: boolean) { sessionStorage.setItem(this.KEY_AUTHENTICATED, value.toString()) this.authenticatedUser$.next({ authenticated: value, @@ -44,12 +54,16 @@ export class AppState { }) } - get authenticationExpiration(): number { - return parseInt(sessionStorage.getItem(this.KEY_AUTHENTICATION_EXPIRATION)) + get isDefaultGroupUserAuthenticated(): boolean { + return sessionStorage.getItem(this.KEY_DEFAULT_GROUP_USER_AUTHENTICATED) === 'true' } - set authenticationExpiration(value: number) { - sessionStorage.setItem(this.KEY_AUTHENTICATION_EXPIRATION, value.toString()) + private set isDefaultGroupUserAuthenticated(value: boolean) { + sessionStorage.setItem(this.KEY_DEFAULT_GROUP_USER_AUTHENTICATED, value.toString()) + } + + get hasCredentials(): boolean { + return this.isAuthenticated || this.isDefaultGroupUserAuthenticated } get authenticatedUser(): User { @@ -57,9 +71,9 @@ export class AppState { return userString ? JSON.parse(userString) : null } - set authenticatedUser(value: User) { + private set authenticatedUser(value: User) { if (value === null) { - sessionStorage.removeItem(this.KEY_LOGGED_IN_USER) + // sessionStorage.removeItem(this.KEY_LOGGED_IN_USER) } else { sessionStorage.setItem(this.KEY_LOGGED_IN_USER, JSON.stringify(value)) } diff --git a/src/app/modules/shared/components/action-bar/action-bar.html b/src/app/modules/shared/components/action-bar/action-bar.html index 4bad334..6aa7ad0 100644 --- a/src/app/modules/shared/components/action-bar/action-bar.html +++ b/src/app/modules/shared/components/action-bar/action-bar.html @@ -1,3 +1,3 @@ -
+
diff --git a/src/app/modules/shared/components/action-bar/action-bar.ts b/src/app/modules/shared/components/action-bar/action-bar.ts index 506bed9..61df16b 100644 --- a/src/app/modules/shared/components/action-bar/action-bar.ts +++ b/src/app/modules/shared/components/action-bar/action-bar.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core' +import {Component, Input} from '@angular/core' @Component({ selector: 'cre-action-group', @@ -11,4 +11,6 @@ export class CreActionGroup {} selector: 'cre-action-bar', templateUrl: 'action-bar.html' }) -export class CreActionBar {} +export class CreActionBar { + @Input() reverse = false +} diff --git a/src/app/modules/shared/components/action-bar/action-group.html b/src/app/modules/shared/components/action-bar/action-group.html index dfdbcc7..01b5f5d 100644 --- a/src/app/modules/shared/components/action-bar/action-group.html +++ b/src/app/modules/shared/components/action-bar/action-group.html @@ -1,3 +1,6 @@ -
- +
+
+ +
+
diff --git a/src/app/modules/shared/components/alerts/alerts.html b/src/app/modules/shared/components/alerts/alerts.html new file mode 100644 index 0000000..f9a034d --- /dev/null +++ b/src/app/modules/shared/components/alerts/alerts.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/modules/shared/components/alerts/alerts.module.ts b/src/app/modules/shared/components/alerts/alerts.module.ts new file mode 100644 index 0000000..110fde4 --- /dev/null +++ b/src/app/modules/shared/components/alerts/alerts.module.ts @@ -0,0 +1,14 @@ +import {NgModule} from '@angular/core'; +import {WarningAlert} from './alerts'; + +@NgModule({ + declarations: [ + WarningAlert + ], + exports: [ + WarningAlert + ], + imports: [] +}) +export class CreAlertsModule { +} diff --git a/src/app/modules/shared/components/alerts/alerts.ts b/src/app/modules/shared/components/alerts/alerts.ts new file mode 100644 index 0000000..e31da46 --- /dev/null +++ b/src/app/modules/shared/components/alerts/alerts.ts @@ -0,0 +1,10 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + +@Component({ + selector: 'cre-warning-alert', + templateUrl: 'alerts.html', + encapsulation: ViewEncapsulation.None +}) +export class WarningAlert { + +} diff --git a/src/app/modules/shared/components/buttons/buttons.sass b/src/app/modules/shared/components/buttons/buttons.sass new file mode 100644 index 0000000..3c36340 --- /dev/null +++ b/src/app/modules/shared/components/buttons/buttons.sass @@ -0,0 +1,6 @@ +cre-button, cre-primary-button, cre-accent-button, cre-warn-button + display: inline-block + width: inherit + + button + width: 100% diff --git a/src/app/modules/shared/components/buttons/buttons.ts b/src/app/modules/shared/components/buttons/buttons.ts index 7b3679e..80ab652 100644 --- a/src/app/modules/shared/components/buttons/buttons.ts +++ b/src/app/modules/shared/components/buttons/buttons.ts @@ -1,51 +1,60 @@ -import {Component, Input} from '@angular/core' +import {Component, Input, ViewEncapsulation} from '@angular/core' import {ThemePalette} from '@angular/material/core' @Component({ selector: 'cre-button', template: ` - - ` + `, + styleUrls: ['buttons.sass'], + encapsulation: ViewEncapsulation.None }) export class CreButtonComponent { @Input() color: ThemePalette + @Input() type = 'button' @Input() disabled = false } @Component({ selector: 'cre-primary-button', template: ` - + - ` + `, + styleUrls: ['buttons.sass'] }) export class CrePrimaryButtonComponent { + @Input() type = 'button' @Input() disabled = false } @Component({ selector: 'cre-accent-button', template: ` - + - ` + `, + styleUrls: ['buttons.sass'] }) export class CreAccentButtonComponent { + @Input() type = 'button' @Input() disabled = false } @Component({ selector: 'cre-warn-button', template: ` - + - ` + `, + styleUrls: ['buttons.sass'] }) export class CreWarnButtonComponent { + @Input() type = 'button' @Input() disabled = false } diff --git a/src/app/modules/shared/components/dialogs/dialogs.module.ts b/src/app/modules/shared/components/dialogs/dialogs.module.ts new file mode 100644 index 0000000..ac98cdc --- /dev/null +++ b/src/app/modules/shared/components/dialogs/dialogs.module.ts @@ -0,0 +1,21 @@ +import {NgModule} from '@angular/core' +import {CreDialogBody, CrePromptDialog} from './dialogs' +import {MatDialogModule} from '@angular/material/dialog' +import {CreButtonsModule} from '../buttons/buttons.module' + +@NgModule({ + declarations: [ + CrePromptDialog, + CreDialogBody + ], + exports: [ + CrePromptDialog, + CreDialogBody + ], + imports: [ + MatDialogModule, + CreButtonsModule + ] +}) +export class CreDialogsModule { +} diff --git a/src/app/modules/shared/components/dialogs/dialogs.scss b/src/app/modules/shared/components/dialogs/dialogs.scss new file mode 100644 index 0000000..3653e6c --- /dev/null +++ b/src/app/modules/shared/components/dialogs/dialogs.scss @@ -0,0 +1,26 @@ +@import "~src/variables"; + +.cre-dialog-panel { + min-width: 20rem; + + mat-dialog-container { + padding: 0; + + .mat-dialog-title, .mat-dialog-content, .mat-dialog-actions { + margin: 0; + padding: $spacer; + } + + .mat-dialog-title { + background-color: $color-primary; + color: $text-color-primary; + } + + .mat-dialog-actions { + min-height: auto; + justify-content: end; + gap: map-get($spacers, 1); + padding-top: 0; + } + } +} diff --git a/src/app/modules/shared/components/dialogs/dialogs.ts b/src/app/modules/shared/components/dialogs/dialogs.ts new file mode 100644 index 0000000..a06f602 --- /dev/null +++ b/src/app/modules/shared/components/dialogs/dialogs.ts @@ -0,0 +1,74 @@ +import {Component, Directive, EventEmitter, Input, Output, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core' +import {MatDialog, MatDialogRef} from '@angular/material/dialog' + +@Directive({ + selector: 'cre-dialog-body' +}) +export class CreDialogBody { +} + +@Directive() +abstract class CreDialog { + @ViewChild(TemplateRef) dialogTemplate: TemplateRef + + @Output() cancel = new EventEmitter(); + @Output() continue = new EventEmitter(); + + private dialogRef: MatDialogRef> | null + + constructor( + protected dialog: MatDialog + ) { + } + + protected abstract get data(): D + + show() { + this.open() + } + + onCancel() { + this.close() + this.cancel.emit(); + } + + onContinue() { + this.close() + this.continue.emit(); + } + + private open() { + const config = { + panelClass: 'cre-dialog-panel', + data: this.data + } + this.dialogRef = this.dialog.open(this.dialogTemplate, config) + } + + private close() { + this.dialogRef.close() + } +} + +@Component({ + selector: 'cre-prompt-dialog', + templateUrl: 'prompt.html', + styleUrls: ['dialogs.scss'], + encapsulation: ViewEncapsulation.None +}) +export class CrePromptDialog extends CreDialog { + @Input() title: string + + protected get data(): CrePromptDialogData { + return { + title: this.title + } + } +} + +abstract class CreDialogData { + title: string +} + +class CrePromptDialogData extends CreDialogData { +} diff --git a/src/app/modules/shared/components/dialogs/prompt.html b/src/app/modules/shared/components/dialogs/prompt.html new file mode 100644 index 0000000..0d14939 --- /dev/null +++ b/src/app/modules/shared/components/dialogs/prompt.html @@ -0,0 +1,10 @@ + +

{{data.title}}

+
+ +
+
+ Annuler + Continuer +
+
diff --git a/src/app/modules/shared/components/forms/buttons.ts b/src/app/modules/shared/components/forms/buttons.ts new file mode 100644 index 0000000..ed1da16 --- /dev/null +++ b/src/app/modules/shared/components/forms/buttons.ts @@ -0,0 +1,18 @@ +import {Component, ContentChild, EventEmitter, Input, Output} from '@angular/core' +import {ICreForm} from './forms'; + +@Component({ + selector: 'cre-form-submit-button', + templateUrl: 'submit-button.html' +}) +export class CreSubmitButton { + @Input() form: ICreForm + @Input() valid: boolean | null + @Input() text = 'Enregistrer' + + @Output() submit = new EventEmitter() + + get disableButton(): boolean { + return !this.form || !(this.valid ?? this.form.valid) + } +} diff --git a/src/app/modules/shared/components/forms/form.html b/src/app/modules/shared/components/forms/form.html index 1d6e89b..4b966b8 100644 --- a/src/app/modules/shared/components/forms/form.html +++ b/src/app/modules/shared/components/forms/form.html @@ -5,7 +5,7 @@ -
+
diff --git a/src/app/modules/shared/components/forms/forms.module.ts b/src/app/modules/shared/components/forms/forms.module.ts index 2ee4a91..4e1a91c 100644 --- a/src/app/modules/shared/components/forms/forms.module.ts +++ b/src/app/modules/shared/components/forms/forms.module.ts @@ -1,28 +1,34 @@ import {NgModule} from '@angular/core' -import {CreFormActions, CreFormComponent, CreFormContent, CreFormTitle} from './forms' +import {CreFormActions, CreForm, CreFormContent, CreFormTitle} from './forms' import {MatCardModule} from '@angular/material/card' import {CommonModule} from '@angular/common' import {MatButtonModule} from '@angular/material/button' import {ReactiveFormsModule} from '@angular/forms' +import {CreSubmitButton} from './buttons'; +import {CreButtonsModule} from '../buttons/buttons.module'; @NgModule({ declarations: [ - CreFormComponent, + CreForm, CreFormTitle, CreFormContent, - CreFormActions + CreFormActions, + CreSubmitButton ], exports: [ - CreFormComponent, + CreForm, CreFormTitle, CreFormContent, - CreFormActions + CreFormActions, + CreSubmitButton ], - imports: [ - MatCardModule, - CommonModule, - MatButtonModule, - ReactiveFormsModule - ] + imports: [ + MatCardModule, + CommonModule, + MatButtonModule, + ReactiveFormsModule, + CreButtonsModule + ] }) -export class CreFormsModule {} +export class CreFormsModule { +} diff --git a/src/app/modules/shared/components/forms/forms.sass b/src/app/modules/shared/components/forms/forms.sass index fffedc8..1a3fb08 100644 --- a/src/app/modules/shared/components/forms/forms.sass +++ b/src/app/modules/shared/components/forms/forms.sass @@ -1,8 +1,12 @@ cre-form display: block + width: max-content + min-width: 50rem + margin-top: 3rem mat-card width: inherit + min-width: inherit cre-form-actions display: flex diff --git a/src/app/modules/shared/components/forms/forms.ts b/src/app/modules/shared/components/forms/forms.ts index 5770d23..ae2f5d3 100644 --- a/src/app/modules/shared/components/forms/forms.ts +++ b/src/app/modules/shared/components/forms/forms.ts @@ -1,6 +1,12 @@ import {Component, ContentChild, Directive, Input, OnInit, ViewEncapsulation} from '@angular/core' import {FormBuilder, FormGroup} from '@angular/forms' +export interface ICreForm { + formGroup: FormGroup + valid: boolean + invalid: boolean +} + @Directive({ selector: 'cre-form-title' }) @@ -17,7 +23,6 @@ export class CreFormContent { selector: 'cre-form-actions' }) export class CreFormActions { - } @Component({ @@ -26,11 +31,11 @@ export class CreFormActions { styleUrls: ['forms.sass'], encapsulation: ViewEncapsulation.None }) -export class CreFormComponent implements OnInit { +export class CreForm implements ICreForm, OnInit { @ContentChild(CreFormActions) formActions: CreFormActions @Input() formControls: { [key: string]: any } - form: FormGroup + formGroup: FormGroup constructor( private formBuilder: FormBuilder @@ -38,14 +43,18 @@ export class CreFormComponent implements OnInit { } ngOnInit(): void { - this.form = this.formBuilder.group(this.formControls) + this.formGroup = this.formBuilder.group(this.formControls) } get hasActions(): boolean { - return this.formActions === true + return !!this.formActions + } + + get valid(): boolean { + return this.formGroup && this.formGroup.valid } get invalid(): boolean { - return !this.form || this.form.invalid + return !this.formGroup || this.formGroup.invalid } } diff --git a/src/app/modules/shared/components/forms/submit-button.html b/src/app/modules/shared/components/forms/submit-button.html new file mode 100644 index 0000000..3d00a97 --- /dev/null +++ b/src/app/modules/shared/components/forms/submit-button.html @@ -0,0 +1 @@ +{{text}} diff --git a/src/app/modules/shared/components/header/header.component.ts b/src/app/modules/shared/components/header/header.component.ts index 94f4563..abe4a17 100644 --- a/src/app/modules/shared/components/header/header.component.ts +++ b/src/app/modules/shared/components/header/header.component.ts @@ -58,20 +58,21 @@ export class HeaderComponent extends SubscribingComponent { } ngOnDestroy(): void { - this.accountService.logout(() => { - console.log('Successfully logged out') - }) + this.subscribe( + this.accountService.logout(), + () => console.info('Successfully logged out') + ) super.ngOnDestroy() } get logoUrl(): string { - return environment.apiUrl + "/file?path=images%2Flogo&mediaType=image/png" + return environment.apiUrl + "/config/logo" } set activeLink(link: string) { this._activeLink = link - this.router.navigate([link]) + this.urlUtils.navigateTo(link) } get activeLink() { diff --git a/src/app/modules/shared/components/info-banner/info-banner.component.sass b/src/app/modules/shared/components/info-banner/info-banner.component.sass index 558af2c..31c5568 100644 --- a/src/app/modules/shared/components/info-banner/info-banner.component.sass +++ b/src/app/modules/shared/components/info-banner/info-banner.component.sass @@ -1,4 +1,4 @@ -@import "~src/custom-theme" +@import "~src/variables" .info-banner-wrapper background-color: $color-primary diff --git a/src/app/modules/shared/components/inputs/autocomplete.html b/src/app/modules/shared/components/inputs/autocomplete.html index bc21b24..760d6f4 100644 --- a/src/app/modules/shared/components/inputs/autocomplete.html +++ b/src/app/modules/shared/components/inputs/autocomplete.html @@ -24,7 +24,7 @@ [ngTemplateOutletContext]="{errors: control.errors}"> - + {{option}} diff --git a/src/app/modules/shared/components/inputs/chips-combo-box.html b/src/app/modules/shared/components/inputs/chips-combo-box.html index 443ff0c..f454ad3 100644 --- a/src/app/modules/shared/components/inputs/chips-combo-box.html +++ b/src/app/modules/shared/components/inputs/chips-combo-box.html @@ -30,7 +30,7 @@ - + {{option.display ? option.display : option.value}} diff --git a/src/app/modules/shared/components/inputs/combo-box.html b/src/app/modules/shared/components/inputs/combo-box.html index 5170f8c..67e0162 100644 --- a/src/app/modules/shared/components/inputs/combo-box.html +++ b/src/app/modules/shared/components/inputs/combo-box.html @@ -1,22 +1,23 @@ - + {{label}} - - Ce champ est requis + + Cette valeur est invalide + Ce champ est requis + [ngTemplateOutletContext]="{errors: internalControl.errors}"> - - {{option.value}} + + {{entry.display ? entry.display : entry.value}} diff --git a/src/app/modules/shared/components/inputs/input.html b/src/app/modules/shared/components/inputs/input.html index d123b75..f58c818 100644 --- a/src/app/modules/shared/components/inputs/input.html +++ b/src/app/modules/shared/components/inputs/input.html @@ -1,25 +1,23 @@ {{label}} - - + + + + + + + + () + @ViewChild('input') input: any @ContentChild(TemplateRef) errors: TemplateRef + + fieldRequired = false + + constructor( + private cdRef: ChangeDetectorRef + ) { + super() + } + + ngAfterViewInit() { + const element = this.input.nativeElement + element.type = this.type + element.step = this.step.toString() + element.placeholder = this.placeholder + element.autocomplete = this.autocomplete ? 'on' : 'off' + + this.fieldRequired = this.control ? this.control.validator && this.control.validator({} as AbstractControl)?.required : this.required + + this.cdRef.detectChanges() + } +} + +@Component({ + selector: 'cre-textarea', + templateUrl: 'textarea.html', + encapsulation: ViewEncapsulation.None +}) +export class CreTextareaComponent { + @Input() label: string + @Input() control: FormControl + @Input() cols = 40 + @Input() rows = 3 + @Input() placeholder: string | null } @Component({ @@ -51,11 +91,11 @@ export class CreInputComponent { encapsulation: ViewEncapsulation.None }) export class CreAutocompleteInputComponent { - @Input() control: FormControl | null + @Input() control: AbstractControl | null @Input() label: string @Input() icon: string @Input() required = true - @Input() options: Observable + @Input() entries: Observable @Input() value @Output() valueChange = new EventEmitter() @@ -69,7 +109,7 @@ export class CreAutocompleteInputComponent { encapsulation: ViewEncapsulation.None }) export class CreChipInputComponent implements OnInit { - @Input() control: FormControl + @Input() control: AbstractControl @Input() label: string @Input() icon: string @Input() required = true @@ -118,13 +158,116 @@ export class CreChipInputComponent implements OnInit { encapsulation: ViewEncapsulation.None }) export class CreComboBoxComponent { - @Input() control: FormControl + @Input() control: AbstractControl @Input() label: string @Input() icon: string @Input() required = true - @Input() options: Observable @ContentChild(TemplateRef) errors: TemplateRef + + internalControl: FormControl + filteredEntries: CreInputEntry[] + validValue = false + + private _destroy$ = new Subject() + private _entries: CreInputEntry[] + private _controlsInitialized = false + + @Input() + set entries(entries: Observable | CreInputEntry[]) { + if (isObservable(entries)) { + (entries as Observable).pipe(takeUntil(this._destroy$)) + .subscribe({ + next: entries => { + this.initControls(entries) + } + }) + } else { + this.initControls((entries as CreInputEntry[])) + } + } + + reloadEntries() { + this.filteredEntries = this.filterEntries(this.internalControl.value) + } + + private initControls(entries) { + this._entries = entries + if (this._controlsInitialized) { + return + } + + this.internalControl = new FormControl({ + value: null, + disabled: false + }, Validators.compose([this.control.validator, this.valueValidator()])) + this.internalControl.valueChanges + .pipe(takeUntil(this._destroy$)) + .subscribe({ + next: value => { + if (this.internalControl.valid) { + this.control.setValue(this.findEntryByValue(value).key) + } else { + this.control.setValue(null) + } + + this.filteredEntries = this.filterEntries(value) + } + }) + + if (this.control.value) { + this.internalControl.setValue(this.findEntryByKey(this.control.value)?.value) + } + + if (this.control.disabled) { + this.internalControl.disable() + } + + this.reloadEntries() + this._controlsInitialized = true + } + + private filterEntries(value: string): CreInputEntry[] { + if (!value) { + return this._entries + } + + const valueLowerCase = value.toLowerCase() + return this._entries.filter(entry => { + if (entry.display) { + return entry.display.toLowerCase().includes(valueLowerCase) + } else { + return entry.value.toLowerCase().includes(valueLowerCase) + } + }) + } + + private findEntryByKey(key: any): CreInputEntry | null { + const found = this._entries.filter(e => e.key === key) + if (found.length <= 0) { + return null + } + return found[0] + } + + private findEntryByValue(value: any): CreInputEntry | null { + const found = this._entries.filter(e => e.value === value) + if (found.length <= 0) { + return null + } + return found[0] + } + + private existsEntryByValue(value: any): boolean { + return this._entries && this._entries.filter(o => o.value === value).length > 0 + } + + private valueValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const valid = this.existsEntryByValue(control.value) + return valid ? null : {invalidValue: {value: control.value}} + } + } } @Component({ @@ -133,25 +276,27 @@ export class CreComboBoxComponent { encapsulation: ViewEncapsulation.None }) export class CreChipComboBoxComponent extends CreChipInputComponent implements OnDestroy { - @Input() options: Observable + @Input() entries: Observable @ContentChild(TemplateRef) errors: TemplateRef @ViewChild('chipInput') chipInput: ElementRef @ViewChild('auto') matAutocomplete: MatAutocomplete - filteredOptions: Observable + filteredEntries: Observable - private _options: ComboBoxEntry[] + private _entries: CreInputEntry[] private _destroy$ = new Subject() ngOnInit() { super.ngOnInit() - this.options.pipe(takeUntil(this._destroy$)) - .subscribe({next: options => this._options = options}) + this.entries.pipe(takeUntil(this._destroy$)) + .subscribe({ + next: entries => this._entries = entries + }) - this.filteredOptions = this.inputControl.valueChanges.pipe( - map((query: string | null) => query ? this._filter(query) : this._options.slice()) + this.filteredEntries = this.inputControl.valueChanges.pipe( + map((query: string | null) => query ? this._filter(query) : this._entries.slice()) ) } @@ -170,13 +315,13 @@ export class CreChipComboBoxComponent extends CreChipInputComponent implements O return this.selectedValues.length <= 0 } - private _filter(query: string): ComboBoxEntry[] { + private _filter(query: string): CreInputEntry[] { const filterValue = query.toString().toLowerCase() - return this._options.filter(option => option.value.toString().toLowerCase().indexOf(filterValue) === 0) + return this._entries.filter(e => e.value.toString().toLowerCase().indexOf(filterValue) === 0) } private findValueByKey(key: any): any { - return this._options.filter(o => o.key === key)[0].value + return this._entries.filter(e => e.key === key)[0].value } } @@ -184,12 +329,16 @@ export class CreChipComboBoxComponent extends CreChipInputComponent implements O selector: 'cre-checkbox-input', templateUrl: 'checkbox.html' }) -export class CreCheckboxInputComponent { +export class CreCheckboxInputComponent implements OnInit { @Input() label: string - @Input() control: FormControl + @Input() control: AbstractControl @Input() checked: boolean @Output() checkedChange = new EventEmitter() + + ngOnInit(): void { + this.control?.setValue(this.control.value === 'true') + } } @Component({ @@ -200,7 +349,7 @@ export class CreFileInputComponent implements OnInit { @Input() label: string @Input() icon: string @Input() accept = '' - @Input() control: FormControl | null + @Input() control: AbstractControl | null @Output() selection = new EventEmitter() @Output() invalidFormat = new EventEmitter() @@ -234,7 +383,7 @@ export class CreFileInputComponent implements OnInit { encapsulation: ViewEncapsulation.None }) export class CrePeriodInputComponent implements OnInit { - @Input() control: FormControl + @Input() control: AbstractControl @Input() label: string @Input() hint: string | null @@ -264,15 +413,56 @@ export class CrePeriodInputComponent implements OnInit { } private setValuesFromPeriod(period: string) { + if (!period) { + return + } + const periodTypeChar = period.slice(-1) period = period.slice(1, -1) - this.selectControl.setValue(periodTypeChar) this.inputControl.setValue(period) } } -export class ComboBoxEntry { +@Component({ + selector: 'cre-slider-input', + templateUrl: 'slider.html' +}) +export class CreSliderInputComponent { + @Input() control: FormControl + @Input() label: string + @Input() min: number + @Input() max: number + @Input() step = 1 + @Input() percents = false + @Input() thumbLabel = true + + formatValueForDisplay(value: number): string { + return this.percents ? `${value}%` : value.toString() + } +} + +@Component({ + selector: 'cre-select', + templateUrl: 'select.html' +}) +export class CreSelectComponent extends _CreInputBase { + @Input() entries: CreInputEntry[] | Observable + + get entriesAreObservable(): boolean { + return isObservable(this.entries) + } + + get arrayEntries(): CreInputEntry[] { + return this.entries as CreInputEntry[] + } + + get observableEntries(): Observable { + return this.entries as Observable + } +} + +export class CreInputEntry { constructor( public key: any, public value: any, diff --git a/src/app/modules/shared/components/inputs/select.html b/src/app/modules/shared/components/inputs/select.html new file mode 100644 index 0000000..14a03ee --- /dev/null +++ b/src/app/modules/shared/components/inputs/select.html @@ -0,0 +1,16 @@ + + {{label}} + + + + {{entry.display || entry.value}} + + + + + {{entry.display || entry.value}} + + + + diff --git a/src/app/modules/shared/components/inputs/slider.html b/src/app/modules/shared/components/inputs/slider.html new file mode 100644 index 0000000..1873422 --- /dev/null +++ b/src/app/modules/shared/components/inputs/slider.html @@ -0,0 +1,13 @@ +
+

{{label}}

+ + +
diff --git a/src/app/modules/shared/components/inputs/textarea.html b/src/app/modules/shared/components/inputs/textarea.html new file mode 100644 index 0000000..18653c0 --- /dev/null +++ b/src/app/modules/shared/components/inputs/textarea.html @@ -0,0 +1,9 @@ + + {{label}} + + diff --git a/src/app/modules/shared/components/subscribing.component.ts b/src/app/modules/shared/components/subscribing.component.ts index a31dcd4..d003d0a 100644 --- a/src/app/modules/shared/components/subscribing.component.ts +++ b/src/app/modules/shared/components/subscribing.component.ts @@ -11,6 +11,8 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy { protected subscribers$ = [] protected destroy$ = new Subject() + loading = false + protected constructor( protected errorService: ErrorService, protected activatedRoute: ActivatedRoute, @@ -74,12 +76,14 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy { protected showLoadingWheel(shouldShowWheel) { if (shouldShowWheel) { + this.loading = true globalLoadingWheel.show() } } protected hideLoadingWheel(shouldShowWheel) { if (shouldShowWheel) { + this.loading = false globalLoadingWheel.hide() } } diff --git a/src/app/modules/shared/components/tables/position-buttons.html b/src/app/modules/shared/components/tables/position-buttons.html new file mode 100644 index 0000000..f953d7b --- /dev/null +++ b/src/app/modules/shared/components/tables/position-buttons.html @@ -0,0 +1,17 @@ + + + + diff --git a/src/app/modules/shared/components/tables/table.html b/src/app/modules/shared/components/tables/table.html index 46560ec..9c9a748 100644 --- a/src/app/modules/shared/components/tables/table.html +++ b/src/app/modules/shared/components/tables/table.html @@ -1,4 +1,6 @@ - +
diff --git a/src/app/modules/shared/components/tables/table.sass b/src/app/modules/shared/components/tables/table.sass index 4458b6e..9cbb67d 100644 --- a/src/app/modules/shared/components/tables/table.sass +++ b/src/app/modules/shared/components/tables/table.sass @@ -1,4 +1,4 @@ -@import "~src/custom-theme" +@import "../../../../../custom-theme" cre-table display: block diff --git a/src/app/modules/shared/components/tables/tables.module.ts b/src/app/modules/shared/components/tables/tables.module.ts index 2e9576d..2e5ebc7 100644 --- a/src/app/modules/shared/components/tables/tables.module.ts +++ b/src/app/modules/shared/components/tables/tables.module.ts @@ -1,20 +1,28 @@ import {NgModule} from '@angular/core' import {MatTableModule} from '@angular/material/table' import {CommonModule} from '@angular/common' -import {CreInteractiveCell, CreTable} from './tables' +import {CreInteractiveCell, CrePositionButtons, CreTable} from './tables' +import {MatButtonModule} from "@angular/material/button"; +import {MatIconModule} from "@angular/material/icon"; +import {MatSortModule} from '@angular/material/sort' @NgModule({ declarations: [ CreTable, - CreInteractiveCell - ], - imports: [ - MatTableModule, - CommonModule + CreInteractiveCell, + CrePositionButtons ], + imports: [ + MatTableModule, + CommonModule, + MatButtonModule, + MatIconModule, + MatSortModule + ], exports: [ CreTable, CreInteractiveCell, + CrePositionButtons ] }) export class CreTablesModule { diff --git a/src/app/modules/shared/components/tables/tables.ts b/src/app/modules/shared/components/tables/tables.ts index 59cec5c..a7e7d5d 100644 --- a/src/app/modules/shared/components/tables/tables.ts +++ b/src/app/modules/shared/components/tables/tables.ts @@ -3,13 +3,15 @@ import { Component, ContentChildren, Directive, + EventEmitter, HostBinding, Input, + Output, QueryList, ViewChild, ViewEncapsulation } from '@angular/core' -import {MatColumnDef, MatHeaderRowDef, MatRowDef, MatTable} from '@angular/material/table' +import {MatColumnDef, MatHeaderRowDef, MatRowDef, MatTable, MatTableDataSource} from '@angular/material/table' @Directive({ selector: '[creInteractiveCell]' @@ -57,17 +59,38 @@ export class CreTable implements AfterContentInit { @ViewChild(MatTable, {static: true}) table: MatTable @Input() columns: string[] - @Input() dataSource: T[] @Input() interactive = true + @Input() filterPredicate: (t: T, filter: string) => boolean = () => true + @Input() sortingDataAccessor: (t: T, header: string) => string | number + + @Input() set filter(filter: string) { + if (this.dataSource) { + this.dataSource.filter = filter + } + } + + @Input() set data(data: T[]) { + this.setupDataSource(data) + } selectedIndex = 0 + dataSource: MatTableDataSource + ngAfterContentInit(): void { this.columnDefs.forEach(columnDef => this.table.addColumnDef(columnDef)) this.rowDefs.forEach(rowDef => this.table.addRowDef(rowDef)) this.headerRowDefs.forEach(headerRowDef => this.table.addHeaderRowDef(headerRowDef)) } + private setupDataSource(data: T[]) { + this.dataSource = new MatTableDataSource(data) + + if (this.filterPredicate) { + this.dataSource.filterPredicate = (t, filter) => this.filterPredicate(t, filter) + } + } + onRowHover(index: number) { if (this.interactive) { this.interactiveCells.forEach(cell => cell.hoverIndex = index) @@ -80,4 +103,33 @@ export class CreTable implements AfterContentInit { this.interactiveCells.forEach(cell => cell.selectedIndex = index) } } + + renderRows() { + this.table.renderRows() + } +} + +@Component({ + selector: 'cre-table-position-buttons', + templateUrl: 'position-buttons.html' +}) +export class CrePositionButtons { + @Input() position = 0 + @Input() min = 0 + @Input() max: number + @Input() hidden = false + @Input() disableDecreaseButton = false + @Input() disableIncreaseButton = false + + @Output() positionChange = new EventEmitter() + + increasePosition() { + this.position += 1 + this.positionChange.emit(this.position) + } + + decreasePosition() { + this.position -= 1 + this.positionChange.emit(this.position) + } } diff --git a/src/app/modules/shared/components/user-info/user-menu.component.sass b/src/app/modules/shared/components/user-info/user-menu.component.sass index d3ffcd7..329544c 100644 --- a/src/app/modules/shared/components/user-info/user-menu.component.sass +++ b/src/app/modules/shared/components/user-info/user-menu.component.sass @@ -1,4 +1,4 @@ -@import "../../../../../custom-theme" +@import "~src/variables" p, labeled-icon margin: 0 diff --git a/src/app/modules/shared/model/config.model.ts b/src/app/modules/shared/model/config.model.ts index 642a736..70011e1 100644 --- a/src/app/modules/shared/model/config.model.ts +++ b/src/app/modules/shared/model/config.model.ts @@ -1,7 +1,10 @@ +import {AbstractControl, Form, FormControl, Validators} from '@angular/forms' +import {filterMap} from '../utils/map.utils' + export class Config { static readonly INSTANCE_NAME = 'instance.name' - static readonly INSTANCE_LOGO_PATH = 'instance.logo.path' - static readonly INSTANCE_ICON_PATH = 'instance.icon.path' + static readonly INSTANCE_LOGO_SET = 'instance.logo.set' + static readonly INSTANCE_ICON_SET = 'instance.icon.set' static readonly INSTANCE_URL = 'instance.url' static readonly DATABASE_URL = 'database.url' static readonly DATABASE_USER = 'database.user' @@ -16,12 +19,63 @@ export class Config { static readonly JAVA_VERSION = 'env.java.version' static readonly OPERATING_SYSTEM = 'env.os' - constructor( - public key: string, - public content: string, - public lastUpdated: string, - public requireRestart: boolean, - public editable: boolean - ) { - } + static readonly IMAGE_CONFIG_KEYS = [ + Config.INSTANCE_LOGO_SET, + Config.INSTANCE_ICON_SET + ] + + static readonly PASSWORD_CONFIG_KEYS = [ + Config.DATABASE_PASSWORD + ] + + public key: string + public requireRestart: boolean + public editable: boolean + public content?: string + public lastUpdated?: string +} + +export class ConfigKeyContent { + public key: string + public content: string +} + +export class ConfigControl { + public config: Config + public control: AbstractControl +} + +export function buildFormControl(config: Config): AbstractControl { + return new FormControl({value: config.content, disabled: !config.editable}, !configKeyIsPassword(config.key) ? Validators.required : null) +} + +export function configKeyIsPassword(key: string): boolean { + return Config.PASSWORD_CONFIG_KEYS.indexOf(key) >= 0 +} + +export function filterConfigKeyControlMap(map: Map): Map { + return filterMap(map, (key, control) => { + return control.dirty && + Config.IMAGE_CONFIG_KEYS.indexOf(key) < 0 && // Filter image configs because they are sent to a different endpoint + control.value !== undefined && + control.value !== null + }) +} + +export function filterImageConfigKeyControlMap(map: Map): Map { + return filterMap(map, (key, control) => { + return Config.IMAGE_CONFIG_KEYS.indexOf(key) >= 0 && control.dirty + }) +} + +export function mapToConfigKeyContent(key: string, control: AbstractControl): ConfigKeyContent { + return {key, content: control.value} +} + +export function mapToConfigKeyContentArray(map: Map): ConfigKeyContent[] { + const array: ConfigKeyContent[] = [] + map.forEach((control, key) => { + array.push(mapToConfigKeyContent(key, control)) + }) + return array } diff --git a/src/app/modules/shared/model/material.model.ts b/src/app/modules/shared/model/material.model.ts index 56187fd..681e1f6 100644 --- a/src/app/modules/shared/model/material.model.ts +++ b/src/app/modules/shared/model/material.model.ts @@ -1,4 +1,4 @@ -import {MaterialType} from "./materialtype.model"; +import {MaterialType} from './materialtype.model' import {openPdf} from '../utils/utils' export class Material { @@ -15,3 +15,38 @@ export class Material { export function openSimdut(material: Material) { openPdf(material.simdutUrl) } + +export const materialComparator = (a: Material, b: Material): number => { + 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 + } + } +} + +// Uses private use UTF-8 char to separate the two fields, change if a better method is found +export const materialFilterFieldSeparator = '􀃿' + +export function materialMatchesFilter(material: Material, filter: string): boolean { + const [materialTypeFilter, materialNameFilter, hideLowQuantity, lowQuantityThreshold] = filter.split(materialFilterFieldSeparator) + const materialTypeId = parseInt(materialTypeFilter) + const matchesMaterialType = materialTypeId === 1 || materialTypeId == material.materialType.id + const matchesMaterialName = !materialNameFilter || material.name.toLowerCase().includes(materialNameFilter.toLowerCase()) + const matchesLowQuantity = material.inventoryQuantity < parseInt(lowQuantityThreshold) + + return matchesMaterialType && matchesMaterialName && (hideLowQuantity === 'false' || matchesLowQuantity) +} diff --git a/src/app/modules/shared/model/recipe.model.ts b/src/app/modules/shared/model/recipe.model.ts index 2fb9e52..7134171 100644 --- a/src/app/modules/shared/model/recipe.model.ts +++ b/src/app/modules/shared/model/recipe.model.ts @@ -1,25 +1,22 @@ import {Material} from './material.model' -import {LocalDate} from 'js-joda' import {Company} from './company.model' import {Group} from './user' +import {UNIT_MILLILITER} from "../units"; export class Recipe { - constructor( - public id: number, - public name: string, - public description: string, - public color: string, - public gloss: number, - public sample: number, - public approbationDate: string, - public approbationExpired: boolean, - public remark: string, - public company: Company, - public mixes: Mix[], - public groupsInformation: RecipeGroupInformation[], - public imagesUrls: string[] - ) { - } + public id: number + public name: string + public description: string + public color: string + public gloss: number + public sample: number + public approbationDate: string + public remark: string + public company: Company + public mixes: Mix[] + public approbationExpired: boolean + public groupsInformation: RecipeGroupInformation[] + public imagesUrls: string[] } export class RecipeGroupInformation { @@ -57,7 +54,8 @@ export class MixMaterialDto { public materialId: number, public quantity: number, public isPercents: boolean, - public position: number + public position: number, + public units: string ) { } } @@ -102,12 +100,13 @@ export function sortRecipeSteps(steps: RecipeStep[]): RecipeStep[] { return steps.sort((a, b) => a.position - b.position) } -export function mixMaterialsAsMixMaterialsDto(mix: Mix): MixMaterialDto[] { +export function mixMaterialsToMixMaterialsDto(mix: Mix): MixMaterialDto[] { return sortMixMaterialsDto(mix.mixMaterials.map(m => new MixMaterialDto( m.material.id, m.quantity, m.material.materialType.usePercentages, - m.position + m.position, + UNIT_MILLILITER ))) } @@ -125,3 +124,8 @@ export function getRecipeLuma(recipe: Recipe): number { return 0.2126 * r + 0.7152 * g + 0.0722 * b // per ITU-R BT.709 } + +export function recipeMatchesFilter(recipe: Recipe, filter: string): boolean { + const recipeStr = recipe.company.name + recipe.name + recipe.description + recipe.sample + return recipeStr.toLowerCase().indexOf(filter.toLowerCase()) >= 0 +} diff --git a/src/app/modules/shared/service/alert.service.ts b/src/app/modules/shared/service/alert.service.ts index fd32550..5a6a0a5 100644 --- a/src/app/modules/shared/service/alert.service.ts +++ b/src/app/modules/shared/service/alert.service.ts @@ -38,7 +38,7 @@ export class AlertService { * An alert handler component is a component that will show the alerts pushed by the alert system to the user. */ @Directive() -// tslint:disable-next-line:directive-class-suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export abstract class AlertHandlerComponent implements OnDestroy { protected static readonly DEFAULT_ALERT_BUFFER_SIZE = 3 protected static readonly DEFAULT_ALERT_DURATION = 5 diff --git a/src/app/modules/shared/service/api.service.ts b/src/app/modules/shared/service/api.service.ts index 7c39b28..462531b 100644 --- a/src/app/modules/shared/service/api.service.ts +++ b/src/app/modules/shared/service/api.service.ts @@ -4,10 +4,9 @@ import {Observable, Subject} from 'rxjs' import {environment} from '../../../../environments/environment' import {AppState} from '../app-state' import {Router} from '@angular/router' -import {map, share, takeUntil, tap} from 'rxjs/operators' +import {map, share, takeUntil} from 'rxjs/operators' import {valueOr} from '../utils/utils' import {ErrorService} from './error.service' -import {globalLoadingWheel} from '../components/loading-wheel/loading-wheel.component' @Injectable({ providedIn: 'root' @@ -71,14 +70,13 @@ export class ApiService implements OnDestroy { observe: 'response' } if (needAuthentication) { - if (this.checkAuthenticated()) { + if (this.appState.hasCredentials) { if (httpOptions) { httpOptions.withCredentials = true } else { console.error('httpOptions need to be specified to use credentials in HTTP methods.') } } else { - this.appState.resetAuthenticatedUser() this.navigateToLogin() } } @@ -90,11 +88,6 @@ export class ApiService implements OnDestroy { .pipe(takeUntil(this._destroy$), map(r => r.body), share()) } - private checkAuthenticated(): boolean { - return (this.appState.isAuthenticated && Date.now() <= this.appState.authenticationExpiration) || - (this.appState.authenticatedUser && this.appState.authenticatedUser.group != null) - } - private navigateToLogin() { this.router.navigate(['/account/login']) } diff --git a/src/app/modules/shared/service/config.service.ts b/src/app/modules/shared/service/config.service.ts index 880b697..546ce89 100644 --- a/src/app/modules/shared/service/config.service.ts +++ b/src/app/modules/shared/service/config.service.ts @@ -1,13 +1,9 @@ import {Injectable} from '@angular/core' -import {Config} from '../model/config.model' +import {Config, filterConfigKeyControlMap, filterImageConfigKeyControlMap, mapToConfigKeyContentArray} from '../model/config.model' import {Observable} from 'rxjs' import {ApiService} from './api.service' -import {FormControl} from '@angular/forms' - -const imageConfigsKeys = [ - Config.INSTANCE_LOGO_PATH, - Config.INSTANCE_ICON_PATH -] +import {AbstractControl, FormGroup} from '@angular/forms' +import {transformMap} from '../utils/map.utils' @Injectable({ providedIn: 'root' @@ -18,44 +14,55 @@ export class ConfigService { ) { } + get all(): Observable { + return this.api.get('/config') + } + get(key: string): Observable { return this.api.get(`/config/${key}`) } - set(configs: Map): Observable { - const body = [] - for (let key in configs) { - const control = configs[key] - if (control.dirty && key.indexOf('path') < 0) { - body.push({key, content: control.value}) - } + setFromForm(form: FormGroup): Observable { + const map = new Map() + for (let key in form.controls) { + map.set(key, form.controls[key]) } + return this.set(map); + } - const subscriptions = [] - imageConfigsKeys.forEach(key => { - if (configs[key].dirty) { - subscriptions.push(this.setImage(key, configs[key].value)) - } - }) - - while (subscriptions.length > 0) { - const subscription = subscriptions.pop().subscribe({ - next: () => subscription.unsubscribe() - }) - } + set(configs: Map): Observable { + const body = mapToConfigKeyContentArray(filterConfigKeyControlMap(configs)) + const imageConfigs = filterImageConfigKeyControlMap(configs) + this.setImages(imageConfigs) return this.api.put('/config', body) } setImage(key: string, image: File): Observable { - const body = new FormData() - body.append('key', key) - body.append('image', image) + const path = key == Config.INSTANCE_ICON_SET ? 'icon' : 'logo'; - return this.api.put('/config/image', body) + const body = new FormData() + body.append(path, image) + + return this.api.put(`/config/${path}`, body) } restart(): Observable { return this.api.post('/config/restart') } + + private setImages(configs: Map) { + const subscriptions = this.getImageConfigsSubscriptions(configs) + while (subscriptions.length > 0) { + const subscription = subscriptions.pop().subscribe({ + next: () => subscription.unsubscribe() + }) + } + } + + private getImageConfigsSubscriptions(configs: Map): Observable[] { + return transformMap(configs, (key, control) => { + return this.setImage(key, control.value) + }) + } } diff --git a/src/app/modules/shared/shared.module.ts b/src/app/modules/shared/shared.module.ts index 1d7d62e..be216ae 100644 --- a/src/app/modules/shared/shared.module.ts +++ b/src/app/modules/shared/shared.module.ts @@ -36,6 +36,9 @@ import {InfoBannerModule} from './components/info-banner/info-banner.module' import {CreFormsModule} from './components/forms/forms.module' import {VarDirective} from './directives/var.directive' import {CreColorPreview} from './components/color-preview/color-preview' +import {CreDialogsModule} from './components/dialogs/dialogs.module' +import {CreAlertsModule} from './components/alerts/alerts.module'; +import {CreActionBarModule} from './components/action-bar/action-bar.module' @NgModule({ declarations: [VarDirective, HeaderComponent, UserMenuComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent, LoadingWheelComponent, CreColorPreview], @@ -71,7 +74,9 @@ import {CreColorPreview} from './components/color-preview/color-preview' InfoBannerModule, CreFormsModule, VarDirective, - CreColorPreview + CreColorPreview, + CreDialogsModule, + CreAlertsModule ], imports: [ MatTabsModule, diff --git a/src/app/modules/shared/units.ts b/src/app/modules/shared/units.ts index 403f136..adbb475 100644 --- a/src/app/modules/shared/units.ts +++ b/src/app/modules/shared/units.ts @@ -25,8 +25,8 @@ export const UNIT_RATIOS = { } } -export function convertMixMaterialQuantity(computedQuantity: MixMaterialDto, from: string, to: string): number { - return !computedQuantity.isPercents ? convertQuantity(computedQuantity.quantity, from, to) : computedQuantity.quantity +export function convertMixMaterialQuantity(mixMaterial: MixMaterialDto, to: string): number { + return !mixMaterial.isPercents ? convertQuantity(mixMaterial.quantity, mixMaterial.units, to) : mixMaterial.quantity } export function convertQuantity(quantity: number, from: string, to: string): number { diff --git a/src/app/modules/shared/utils/map.utils.ts b/src/app/modules/shared/utils/map.utils.ts new file mode 100644 index 0000000..dd43b65 --- /dev/null +++ b/src/app/modules/shared/utils/map.utils.ts @@ -0,0 +1,21 @@ +export function anyMap(map: Map, predicate: (key: K, value: V) => boolean): boolean { + return filterMap(map, predicate).size > 0 +} + +export function filterMap(map: Map, predicate: (key: K, value: V) => boolean): Map { + const filteredMap = new Map() + map.forEach((value, key) => { + if (predicate(key, value)) { + filteredMap.set(key, value) + } + }) + return filteredMap +} + +export function transformMap(map: Map, transform: (key: K, value: V) => T): T[] { + const transformedArray = [] + map.forEach((value, key) => { + transformedArray.push(transform(key, value)) + }) + return transformedArray +} diff --git a/src/app/modules/shared/utils/utils.ts b/src/app/modules/shared/utils/utils.ts index bd54ea9..dfa6bf9 100644 --- a/src/app/modules/shared/utils/utils.ts +++ b/src/app/modules/shared/utils/utils.ts @@ -1,5 +1,5 @@ /** Returns [value] if it is not null or [or]. */ -import {DateTimeFormatter, LocalDate, LocalDateTime} from 'js-joda' +import {DateTimeFormatter, LocalDate, LocalDateTime} from '@js-joda/core' import {TouchUpKit} from '../model/touch-up-kit.model' import {environment} from '../../../../environments/environment' @@ -60,3 +60,12 @@ export function readFile(file: File, consumer: (any) => void) { export function getFileUrl(path: string) { return `${environment.apiUrl}/file?path=${encodeURIComponent(path)}` } + +export function getConfiguredImageUrl(path: string) { + return `${environment.apiUrl}/config/${path}` +} + +export function round(n: number, digits: number): number { + const power = Math.pow(10, digits) + return Math.round(n * power) / power +} diff --git a/src/app/modules/touch-up-kit/components/finish.sass b/src/app/modules/touch-up-kit/components/finish.sass index 899e060..7713c99 100644 --- a/src/app/modules/touch-up-kit/components/finish.sass +++ b/src/app/modules/touch-up-kit/components/finish.sass @@ -1,4 +1,4 @@ -@import '~src/custom-theme' +@import '~src/variables' .touchupkit-finish-container display: inline-block diff --git a/src/app/modules/touch-up-kit/components/finish.ts b/src/app/modules/touch-up-kit/components/finish.ts index d91f01b..09b2fd7 100644 --- a/src/app/modules/touch-up-kit/components/finish.ts +++ b/src/app/modules/touch-up-kit/components/finish.ts @@ -1,6 +1,6 @@ import {Component, Input} from '@angular/core' import {SubscribingComponent} from '../../shared/components/subscribing.component' -import {RecipeService} from '../../colors/services/recipe.service' +import {RecipeService} from '../../recipes/services/recipe.service' import {ErrorService} from '../../shared/service/error.service' import {ActivatedRoute, Router} from '@angular/router' import {Recipe} from '../../shared/model/recipe.model' diff --git a/src/app/modules/touch-up-kit/components/form.html b/src/app/modules/touch-up-kit/components/form.html index 999b820..a6dd9eb 100644 --- a/src/app/modules/touch-up-kit/components/form.html +++ b/src/app/modules/touch-up-kit/components/form.html @@ -5,7 +5,7 @@ @@ -18,7 +18,7 @@ diff --git a/src/app/modules/touch-up-kit/components/form.ts b/src/app/modules/touch-up-kit/components/form.ts index 0a77b7f..5fa1a89 100644 --- a/src/app/modules/touch-up-kit/components/form.ts +++ b/src/app/modules/touch-up-kit/components/form.ts @@ -1,9 +1,9 @@ import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core' -import {chipListRequired, ComboBoxEntry, CreChipComboBoxComponent} from '../../shared/components/inputs/inputs' -import {CreFormComponent} from '../../shared/components/forms/forms' +import {chipListRequired, CreInputEntry, CreChipComboBoxComponent} from '../../shared/components/inputs/inputs' +import {CreForm} from '../../shared/components/forms/forms' import {TouchUpKitProductEditor} from './product-editor' import {FormControl, Validators} from '@angular/forms' -import {RecipeService} from '../../colors/services/recipe.service' +import {RecipeService} from '../../recipes/services/recipe.service' import {CompanyService} from '../../company/service/company.service' import {ErrorService} from '../../shared/service/error.service' import {ActivatedRoute, Router} from '@angular/router' @@ -18,14 +18,14 @@ import {map} from 'rxjs/operators' export class TouchUpKitForm extends SubscribingComponent { @ViewChild('finishInput') finishInput: CreChipComboBoxComponent @ViewChild('materialInput') materialInput: CreChipComboBoxComponent - @ViewChild(CreFormComponent) form: CreFormComponent + @ViewChild(CreForm) form: CreForm @ViewChild(TouchUpKitProductEditor) contentEditor: TouchUpKitProductEditor @Input() touchUpKit: TouchUpKit | null controls: any finish$ = this.recipeService.all.pipe( - map(recipes => recipes.map(recipe => new ComboBoxEntry(recipe.id, recipe.name, `${recipe.name} - ${recipe.company.name}`))) + map(recipes => recipes.map(recipe => new CreInputEntry(recipe.id, recipe.name, `${recipe.name} - ${recipe.company.name}`))) ) companies$ = this.companyService.all.pipe( map(companies => companies.map(company => company.name)) diff --git a/src/app/modules/touch-up-kit/pages/details.html b/src/app/modules/touch-up-kit/pages/details.html index bb5b358..93ebf4c 100644 --- a/src/app/modules/touch-up-kit/pages/details.html +++ b/src/app/modules/touch-up-kit/pages/details.html @@ -14,7 +14,7 @@ - + diff --git a/src/app/modules/touch-up-kit/pages/list.html b/src/app/modules/touch-up-kit/pages/list.html index 1cb372b..f5bacdf 100644 --- a/src/app/modules/touch-up-kit/pages/list.html +++ b/src/app/modules/touch-up-kit/pages/list.html @@ -1,11 +1,11 @@ - Ajouter + Ajouter - + @@ -57,7 +57,7 @@ Kits de retouche complétés - + diff --git a/src/app/modules/touch-up-kit/pages/touchupkit.ts b/src/app/modules/touch-up-kit/pages/touchupkit.ts index 57b3ed5..2cdf146 100644 --- a/src/app/modules/touch-up-kit/pages/touchupkit.ts +++ b/src/app/modules/touch-up-kit/pages/touchupkit.ts @@ -7,10 +7,10 @@ import {AccountService} from '../../accounts/services/account.service' import {ErrorService} from '../../shared/service/error.service' import {ActivatedRoute, Router} from '@angular/router' import {Permission} from '../../shared/model/user' -import {RecipeService} from '../../colors/services/recipe.service' +import {RecipeService} from '../../recipes/services/recipe.service' import {AppState} from '../../shared/app-state' import {map} from 'rxjs/operators' -import {LocalDate, Period} from 'js-joda' +import {LocalDate, Period} from '@js-joda/core' import {ConfigService} from '../../shared/service/config.service' import {Config} from '../../shared/model/config.model' diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 5f9a53b..cc4d450 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -1,21 +1,21 @@ // Custom Theming for Angular Material +@use '@angular/material' as mat; // For more information: https://material.angular.io/guide/theming -@import '~@angular/material/theming'; // Plus imports for other components in your app. -$custom-typography: mat-typography-config( +$custom-typography: mat.define-typography-config( $font-family: "Open Sans" ); // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! -@include mat-core($custom-typography); +@include mat.core($custom-typography); // Define the palettes for your theme using the Material Design palettes available in palette.sass // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ -$theme-primary: mat-palette(( +$theme-primary: mat.define-palette(( 50 : #e0e0e0, 100 : #b3b3b3, 200 : #808080, @@ -46,7 +46,7 @@ $theme-primary: mat-palette(( A400 : #ffffff, A700 : #ffffff, ))); -$theme-accent: mat-palette(( +$theme-accent: mat.define-palette(( 50 : #edf9e0, 100 : #d1f0b3, 200 : #b3e680, @@ -78,7 +78,7 @@ $theme-accent: mat-palette(( A700 : #000000, ) )); -$theme-warning: mat-palette(( +$theme-warning: mat.define-palette(( 50 : #fff8e4, 100 : #feefbd, 200 : #fee491, @@ -112,19 +112,15 @@ $theme-warning: mat-palette(( )); // The warn palette is optional (defaults to red). -$theme-error: mat-palette($mat-red); +$theme-error: mat.define-palette(mat.$red-palette); // Create the theme object (a Sass map containing all of the palettes). -$color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-accent, $theme-error); +$color-recipes-explorer-frontend-theme: mat.define-light-theme($theme-primary, $theme-accent, $theme-error); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. -@include angular-material-theme($color-recipes-explorer-frontend-theme); - -$color-primary: map-get($theme-primary, 500); -$color-accent: map-get($theme-accent, 500); -$color-warn: map-get($theme-error, 500); +@include mat.all-component-themes($color-recipes-explorer-frontend-theme); html, body { diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 07d5091..8a40323 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -14,4 +14,4 @@ export const environment = { * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/polyfills.ts b/src/polyfills.ts index 03711e5..dcd18ea 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -18,16 +18,6 @@ * BROWSER POLYFILLS */ -/** IE10 and IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - */ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags @@ -55,7 +45,7 @@ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js/dist/zone'; // Included with Angular CLI. +import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** diff --git a/src/styles.sass b/src/styles.sass index e026f75..c096222 100644 --- a/src/styles.sass +++ b/src/styles.sass @@ -1,6 +1,4 @@ -@import 'assets/sass/modules/_fonts.sass' -@import "custom-theme" -@import "~material-design-icons/iconfont/material-icons.css" +@import "variables" mat-card padding: 0 !important diff --git a/src/test.ts b/src/test.ts index 50193eb..4bf4afb 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,6 +1,6 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/dist/zone-testing'; +import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, @@ -17,7 +17,9 @@ declare const require: { // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, - platformBrowserDynamicTesting() + platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false } +} ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); diff --git a/tslint.json b/tslint.json deleted file mode 100644 index a45c509..0000000 --- a/tslint.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "extends": "tslint:recommended", - "rules": { - "align": { - "options": [ - "parameters", - "statements" - ] - }, - "array-type": false, - "arrow-parens": false, - "arrow-return-shorthand": true, - "deprecation": { - "severity": "warning" - }, - "component-class-suffix": true, - "contextual-lifecycle": true, - "curly": true, - "directive-class-suffix": true, - "directive-selector": [ - true, - "attribute", - "cre", - "camelCase" - ], - "component-selector": [ - true, - "element", - "cre", - "kebab-case" - ], - "eofline": true, - "import-blacklist": [ - true, - "rxjs/Rx" - ], - "import-spacing": true, - "indent": { - "options": [ - "spaces" - ] - }, - "interface-name": false, - "max-classes-per-file": false, - "max-line-length": [ - false, - 140 - ], - "member-access": false, - "member-ordering": [ - true, - { - "order": [ - "static-field", - "instance-field", - "static-method", - "instance-method" - ] - } - ], - "no-consecutive-blank-lines": false, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-empty": false, - "no-inferrable-types": [ - true, - "ignore-params" - ], - "no-non-null-assertion": true, - "no-redundant-jsdoc": true, - "no-switch-case-fall-through": true, - "no-var-requires": false, - "object-literal-key-quotes": [ - true, - "as-needed" - ], - "object-literal-sort-keys": false, - "ordered-imports": false, - "quotemark": [ - true, - "single" - ], - "trailing-comma": false, - "no-conflicting-lifecycle": true, - "no-host-metadata-property": true, - "no-input-rename": true, - "no-inputs-metadata-property": true, - "no-output-native": true, - "no-output-on-prefix": true, - "no-output-rename": true, - "no-outputs-metadata-property": true, - "space-before-function-paren": { - "options": { - "anonymous": "never", - "asyncArrow": "always", - "constructor": "never", - "method": "never", - "named": "never" - } - }, - "template-banana-in-box": true, - "template-no-negated-async": true, - "use-lifecycle-interface": true, - "use-pipe-transform-interface": true, - "typedef-whitespace": { - "options": [ - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - }, - { - "call-signature": "onespace", - "index-signature": "onespace", - "parameter": "onespace", - "property-declaration": "onespace", - "variable-declaration": "onespace" - } - ] - }, - "semicolon": [false, "never"], - "triple-equals": false, - "variable-name": { - "options": [ - "ban-keywords", - "check-format", - "allow-pascal-case" - ] - }, - "whitespace": { - "options": [ - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type", - "check-typecast" - ] - }, - "ban-types": false - }, - "rulesDirectory": [ - "codelyzer" - ] -}
Nom {{product.name}}Project {{touchUpKit.project}} Project {{touchUpKit.project}}