Compare commits

..

46 Commits

Author SHA1 Message Date
William Nolin 3c630914b0 Merge pull request 'develop' (#8) from develop into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #8
2022-04-20 23:13:53 -04:00
William Nolin c0507ec11f Merge pull request 'feature/25-dtos' (#7) from feature/25-dtos into develop
continuous-integration/drone/pr Build is passing Details
Reviewed-on: #7
2022-04-20 22:52:50 -04:00
FyloZ 09e89e68bb
#25 apply backend changes 2022-04-20 22:51:35 -04:00
FyloZ cc24423389
#25 Update files base url
continuous-integration/drone/push Build is passing Details
2022-02-27 21:01:07 -05:00
FyloZ 6c8d35560e
CI/CD
continuous-integration/drone/push Build is passing Details
2022-01-23 01:28:58 -05:00
FyloZ af3b8eee8c
Merge remote-tracking branch 'origin/develop' into develop
continuous-integration/drone/push Build is passing Details
# Conflicts:
#	.drone.yml
2021-12-23 21:04:43 -05:00
FyloZ f21157d283
Fix mix editor did not get the materials from the correct location in update mode 2021-12-23 21:03:58 -05:00
William Nolin 0960eaa507 Merge pull request '12-user-info-jwt' (#6) from 12-user-info-jwt into develop
Reviewed-on: #6
2021-12-22 16:27:46 -05:00
FyloZ f04a3c7ba3
Fix mix editor did not get the materials from the correct location in update mode 2021-12-20 19:20:01 -05:00
FyloZ 82cb974d27
CI/CD 2021-12-15 22:32:40 -05:00
FyloZ d239d37e66
Remote login/logout old components 2021-12-15 22:25:22 -05:00
William Nolin 30f8c44a54 Merge pull request 'develop' (#5) from develop into master
continuous-integration/drone/push Build is failing Details
Reviewed-on: #5
2021-12-15 00:25:09 -05:00
William Nolin 24a713160a Merge pull request '12-user-info-jwt' (#4) from 12-user-info-jwt into develop
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/pr Build is passing Details
Reviewed-on: #4
2021-12-15 00:24:14 -05:00
FyloZ be592a22f5
Fix login and group user login. Hides add buttons when the user doesn't have the required permission.
continuous-integration/drone/pr Build was killed Details
2021-12-14 23:38:32 -05:00
William Nolin cb7f38b46b Improve AppState 2021-12-14 19:28:50 -05:00
William Nolin 07b410d053 Login 2021-12-09 18:53:16 -05:00
FyloZ 019b20fd02
#12 Handle login errors 2021-12-06 23:33:41 -05:00
FyloZ 9deecb3cbd
Update login page 2021-12-03 16:29:15 -05:00
William Nolin 23b80daa75 Merge pull request 'Several updates' (#3) from 14-no-entity-message into develop
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3
2021-12-02 15:00:52 -05:00
FyloZ b6f687e0f1
Downgrade to Angular 12
continuous-integration/drone/pr Build is passing Details
2021-12-02 14:37:14 -05:00
FyloZ 2351ecd258
Update to Angular 13 2021-12-02 14:07:15 -05:00
FyloZ 6ba1073004
Update to Angular 13 2021-12-02 13:58:29 -05:00
FyloZ 3136f2e1d6
Update to Angular 13 2021-12-01 23:19:35 -05:00
FyloZ f644edae21
Setup update to Angular 13 2021-12-01 23:15:51 -05:00
FyloZ 5b564460d4
Setup update to Angular 13 2021-12-01 23:13:48 -05:00
FyloZ f37363e457
Update to Angular Material 12 2021-12-01 23:12:23 -05:00
FyloZ 75f356cc5a
Update to Angular 12 2021-12-01 23:03:24 -05:00
FyloZ e18fe04f36
Update inventory 2021-12-01 22:52:00 -05:00
William Nolin 3a5f90b8c5 Low quantity filter 2021-12-01 22:23:02 -05:00
FyloZ a8d13cc1fb
Start updating inventory 2021-12-01 20:41:26 -05:00
FyloZ f564c37b15
Update recipe list 2021-12-01 10:55:45 -05:00
FyloZ 4ccdd090c1
Update material type list 2021-12-01 00:10:23 -05:00
FyloZ ba360529cc
Update company list 2021-11-30 23:34:06 -05:00
William Nolin a0d15b57fb Add no entity warning for companies and inventory 2021-11-23 12:04:10 -05:00
FyloZ e332fee3ba
Add edit support to new mix editor 2021-11-18 01:09:35 -05:00
William Nolin 6a5f74016f Only some bugs to fix 2021-11-17 21:30:03 -05:00
FyloZ 971e3dcb3c
Working mix editor 2021-11-17 19:23:13 -05:00
William Nolin c3122c2e3e Start rewriting mix editor 2021-11-16 19:58:52 -05:00
William Nolin cb51e66d11 Broken 2021-11-15 21:52:17 -05:00
William Nolin a1baa334ed Fix all fields are forced required 2021-11-10 17:25:32 -05:00
William Nolin c21dd8d0f9 Things 2021-11-08 01:26:31 -05:00
William Nolin a96062a91f #14 Update recipe explorer 2021-09-21 07:40:36 -04:00
William Nolin 521db72f5e #14 Update recipe editor 2021-09-14 16:19:00 -04:00
William Nolin c35635c596 #14 Add messages when adding recipe 2021-09-14 09:28:28 -04:00
William Nolin 73bb2c3bce #14 Add messages in the color list 2021-09-10 07:55:24 -04:00
William Nolin 951f9fcb87 #14 Add message in the color list 2021-09-10 07:40:38 -04:00
146 changed files with 2572 additions and 1860 deletions

View File

@ -1,12 +1,13 @@
---
global-variables:
release: &release ${DRONE_BRANCH##**/}
release: &release ${DRONE_TAG}
environment: &environment
CRE_REGISTRY_IMAGE: registry.fyloz.dev:5443/colorrecipesexplorer/frontend
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/frontend
CRE_PORT: 9102
CRE_RELEASE: *release
alpine-image: &alpine-image alpine:latest
docker-registry-repo: &docker-registry-repo registry.fyloz.dev:5443/colorrecipesexplorer/frontend
docker-registry: &docker-registry registry.fyloz.dev
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/frontend
kind: pipeline
name: default
@ -21,6 +22,9 @@ steps:
- echo -n "latest" > .tags
when:
branch: develop
event:
exclude:
- pull_request
- name: set-docker-tags-release
image: *alpine-image
@ -29,18 +33,40 @@ steps:
commands:
- echo -n "latest-release,$CRE_RELEASE" > .tags
when:
branch: release/**
event:
- tag
- name: containerize
- name: containerize-dev
image: plugins/docker
environment:
<<: *environment
settings:
repo: *docker-registry-repo
registry: *docker-registry
username:
from_secret: docker_username
password:
from_secret: docker_password
when:
branch:
- develop
- release/**
branch: develop
event:
exclude:
- pull_request
- name: containerize-release
image: plugins/docker
environment:
<<: *environment
settings:
repo: *docker-registry-repo
registry: *docker-registry
username:
from_secret: docker_username
password:
from_secret: docker_password
when:
event:
- tag
- name: deploy
image: alpine:latest
@ -70,10 +96,5 @@ steps:
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker pull $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker run -d -p $CRE_PORT:80 --name=$DEPLOY_CONTAINER_NAME $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
when:
branch: release/**
trigger:
branch:
- develop
- release/**
- master
event:
- tag

1
.gitignore vendored
View File

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

View File

@ -22,7 +22,6 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets",
@ -37,7 +36,13 @@
"src/custom-theme.scss",
"src/styles.sass"
],
"scripts": []
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
@ -67,7 +72,8 @@
}
]
}
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",

View File

@ -1,19 +1,11 @@
version: "3.1"
services:
database:
image: mysql
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- "3306:3306"
backend:
cre.backend:
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database:3306/cre"
cre_database_url: "mysql://database/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 1
@ -23,6 +15,14 @@ services:
volumes:
- cre_data:/usr/bin/cre/data
- cre_config:/usr/bin/cre/config
cre.database:
image: mysql
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- "3307:3306"
volumes:
cre_data:

View File

@ -3,62 +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-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": "^11.2.11",
"@angular/compiler-cli": "~11.2.10",
"@angular/language-service": "~11.2.10",
"@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",
"@typescript-eslint/eslint-plugin": "4.16.1",
"@typescript-eslint/parser": "4.16.1",
"eslint": "^7.6.0",
"eslint": "^8.3.0",
"eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-prefer-arrow": "latest",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"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",
"typescript": "~4.0.7"
"ts-node": "^10.4.0",
"typescript": "~4.3.5"
}
}

View File

@ -18,3 +18,7 @@ $text-color-primary: white;
$color-accent: map-get($theme-accent, 500);
$color-warn: map-get($theme-error, 500);
$light-primary-text: white;
$dark-primary-text: black;
$dark-secondary-text: black;

View File

@ -7,7 +7,7 @@ import {CreConfigEditor} from './modules/configuration/config-editor'
const routes: Routes = [{
path: 'color',
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule)
loadChildren: () => import('./modules/recipes/recipes.module').then(m => m.RecipesModule)
}, {
path: 'account',
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)

View File

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

View File

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

View File

@ -0,0 +1,91 @@
import {Component, HostListener, ViewChild} from '@angular/core'
import {FormControl, Validators} from '@angular/forms'
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
import {AccountService} from './services/account.service'
import {AppState} from '../shared/app-state'
import {ErrorHandler, ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {CreForm, ICreForm} from "../shared/components/forms/forms";
import {AlertService} from "../shared/service/alert.service";
@Component({
selector: 'cre-login',
templateUrl: 'login.html',
styles: [
'cre-form { min-width: 25rem; margin-top: 50vh; transform: translateY(-70%) }'
]
})
export class Login extends ErrorHandlingComponent {
@ViewChild(CreForm) form: ICreForm
userIdControl = new FormControl(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$'))]))
passwordControl = new FormControl(null, Validators.required)
errorHandlers: ErrorHandler[] = [{
filter: error => error.status === 403,
messageProducer: () => 'Les identifiants entrés sont invalides'
}]
constructor(
private accountService: AccountService,
private alertService: AlertService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = 'Connexion'
}
// Allows to send the form by pressing Enter
@HostListener('window:keyup.enter', ['$event'])
onEnterKeyEvent() {
if (this.form.formGroup) {
this.submit()
}
}
submit() {
this.subscribeAndNavigate(
this.accountService.login(this.userIdControl.value, this.passwordControl.value),
'/color/list'
)
}
get controls(): { userId: FormControl, password: FormControl } {
return {
userId: this.userIdControl,
password: this.passwordControl
}
}
}
@Component({
selector: 'cre-logout',
template: ''
})
export class Logout extends SubscribingComponent {
constructor(
private accountService: AccountService,
private alertService: AlertService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = 'Connexion'
}
ngOnInit(): void {
if (!this.appState.isAuthenticated) {
this.urlUtils.navigateTo('/account/login')
}
this.subscribeAndNavigate(
this.accountService.logout(),
'/account/login'
)
}
}

View File

@ -0,0 +1,28 @@
<cre-form #form [formControls]="controls" class="mx-auto">
<cre-form-title>Connexion au système</cre-form-title>
<cre-form-content>
<cre-input
[control]="userIdControl"
label="Numéro d'utilisateur"
icon="account">
<ng-template let-errors="errors">
<span *ngIf="errors && errors.pattern">Le numéro d'utilisateur doit être un nombre</span>
</ng-template>
</cre-input>
<cre-input
[control]="passwordControl"
type="password"
label="Mot de passe"
icon="lock">
</cre-input>
</cre-form-content>
<cre-form-actions>
<cre-accent-button
type="submit"
[disabled]="!form.valid"
(click)="submit()">
Connexion
</cre-accent-button>
</cre-form-actions>
</cre-form>

View File

@ -1,36 +0,0 @@
<form [formGroup]="form">
<mat-card class="x-centered y-centered">
<mat-card-header>
<mat-card-title>Connexion au système</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-form-field>
<mat-label>Numéro d'utilisateur</mat-label>
<input matInput [formControl]="idFormControl" type="text"/>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="idFormControl.invalid">
<span *ngIf="idFormControl.errors.required">Un numéro d'utilisateur est requis</span>
<span *ngIf="idFormControl.errors.pattern">Le numéro d'utilisateur doit être un nombre</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Mot de passe</mat-label>
<input matInput [formControl]="passwordFormControl" type="password"/>
<mat-icon matSuffix>lock</mat-icon>
<mat-error *ngIf="passwordFormControl.invalid">
<span *ngIf="passwordFormControl.errors.required">Un mot de passe est requis</span>
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions class="justify-content-end">
<button
mat-raised-button
type="submit"
color="accent"
[disabled]="form.invalid"
(click)="submit()">
Connexion
</button>
</mat-card-actions>
</mat-card>
</form>

View File

@ -1,8 +0,0 @@
mat-card
width: 25rem
.alert p
margin: 0
mat-form-field
width: 100%

View File

@ -1,53 +0,0 @@
import {Component, OnInit} from '@angular/core'
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'
import {AccountService} from '../../services/account.service'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {AppState} from '../../../shared/app-state'
@Component({
selector: 'cre-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.sass']
})
export class LoginComponent extends ErrorHandlingComponent implements OnInit {
form: FormGroup
idFormControl: FormControl
passwordFormControl: FormControl
constructor(
private formBuilder: FormBuilder,
private accountService: AccountService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = 'Connexion'
}
ngOnInit(): void {
this.errorService.activeErrorHandler = this
if (this.accountService.isLoggedIn()) {
this.router.navigate(['/color'])
}
this.idFormControl = this.formBuilder.control(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$'))]))
this.passwordFormControl = this.formBuilder.control(null, Validators.required)
this.form = this.formBuilder.group({
id: this.idFormControl,
password: this.passwordFormControl
})
}
submit() {
this.accountService.login(
this.idFormControl.value,
this.passwordFormControl.value,
() => this.router.navigate(['/color'])
)
}
}

View File

@ -1,28 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {AccountService} from "../../services/account.service";
import {Router} from "@angular/router";
@Component({
selector: 'cre-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.sass']
})
export class LogoutComponent implements OnInit {
constructor(
private accountService: AccountService,
private router: Router
) {
}
ngOnInit(): void {
if (!this.accountService.isLoggedIn()) {
this.router.navigate(['/account/login'])
}
this.accountService.logout(() => {
this.router.navigate(['/account/login'])
})
}
}

View File

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

View File

@ -0,0 +1,13 @@
import {Injectable} from "@angular/core";
import jwtDecode from "jwt-decode";
import { User } from "../../shared/model/user";
@Injectable({
providedIn: 'root'
})
export class JwtService {
parseUser(jwt: string): User {
const decoded = jwtDecode(jwt) as any
return JSON.parse(decoded.user)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,8 +16,8 @@
<button
mat-raised-button
color="primary"
[disabled]="!hasSimdut"
[attr.title]="!hasSimdut ? 'Ce produit n\'a pas de fiche signalitique' : null"
[disabled]="!material.hasSimdut"
[attr.title]="!material.hasSimdut ? 'Ce produit n\'a pas de fiche signalitique' : null"
(click)="openSimdut()">
Voir la fiche signalitique
</button>

View File

@ -119,10 +119,6 @@ export class EditComponent extends ErrorHandlingComponent {
)
}
get hasSimdut(): boolean {
return this.material.simdutUrl != null
}
openSimdut() {
openSimdut(this.material)
}

View File

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

View File

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

View File

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

View File

@ -17,10 +17,6 @@ export class MaterialService {
return this.api.get<Material[]>('/material')
}
get allNotMixType(): Observable<Material[]> {
return this.api.get<Material[]>('/material/notmixtype')
}
getAllForMixCreation(recipeId: number): Observable<Material[]> {
return this.api.get<Material[]>(`/material/mix/create/${recipeId}`)
}
@ -33,14 +29,6 @@ export class MaterialService {
return this.api.get<Material>(`/material/${id}`)
}
hasSimdut(id: number): Observable<boolean> {
return this.api.get<boolean>(`/material/${id}/simdut/exists`)
}
getSimduts(): Observable<number[]> {
return this.api.get<number[]>('/material/simdut')
}
save(name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable<void> {
const body = new FormData()
body.append('name', name)

View File

@ -0,0 +1,10 @@
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-form-submit-button [form]="recipeForm.creForm" (submit)="recipeForm.submit()"></cre-form-submit-button>
</cre-action-group>
</cre-action-bar>
<recipe-form #recipeForm (submitForm)="submit($event)"></recipe-form>

View File

@ -1,15 +1,17 @@
<mat-card *ngIf="editionMode || imagesUrls">
<mat-card *ngIf="editionMode || imagesIds">
<mat-card-header>
<mat-card-title>Images</mat-card-title>
</mat-card-header>
<mat-card-content [class.no-action]="!editionMode">
<div class="d-flex flex-row justify-content-around flex-wrap">
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
<p *ngIf="imagesIds.length <= 0" class="light-text text-center mb-0">Aucune image n'est associée à cette couleur</p>
<div *ngFor="let imageId of imagesIds" class="d-flex flex-column align-self-center m-3">
<div class="image-wrapper">
<img [src]="imageUrl" width="300px"/>
<img [src]="getImageUrl(imageId)" width="300px"/>
<div class="d-flex flex-row justify-content-end mt-2" [class.justify-content-between]="editionMode">
<button mat-raised-button color="primary" (click)="openImage(imageUrl)">Afficher</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageUrl)">Retirer</button>
<button mat-raised-button color="primary" (click)="openImage(imageId)">Afficher</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageId)">Retirer</button>
</div>
</div>
</div>

View File

@ -4,7 +4,8 @@ import {SubscribingComponent} from '../../../shared/components/subscribing.compo
import {ActivatedRoute, Router} from '@angular/router'
import {RecipeImageService} from '../../services/recipe-image.service'
import {ErrorService} from '../../../shared/service/error.service'
import {openJpg} from '../../../shared/utils/utils'
import {getImageUrl, openJpg} from '../../../shared/utils/utils'
import {RecipeService} from "../../services/recipe.service";
@Component({
selector: 'cre-images-editor',
@ -15,10 +16,11 @@ export class ImagesEditorComponent extends SubscribingComponent {
@Input() recipe: Recipe
@Input() editionMode = false
imagesUrls: string[]
imagesIds: string[] = []
constructor(
private recipeImageService: RecipeImageService,
private recipeService: RecipeService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
@ -29,19 +31,26 @@ export class ImagesEditorComponent extends SubscribingComponent {
ngOnInit() {
super.ngOnInit()
this.imagesUrls = this.recipe.imagesUrls
this.subscribe(
this.recipeService.getImagesIds(this.recipe.id),
imagesIds => this.imagesIds = imagesIds ?? []
)
}
submit(event) {
const image = event.target.files[0]
this.subscribe(
this.recipeImageService.save(image, this.recipe.id),
r => this.imagesUrls = r.imagesUrls
imageId => this.imagesIds.push(imageId)
)
}
openImage(url: string) {
openJpg(url)
getImageUrl(id: string): string {
return getImageUrl(this.getImagePath(id))
}
openImage(id: string) {
openJpg(this.getImagePath(id))
}
delete(url: string) {
@ -52,6 +61,10 @@ export class ImagesEditorComponent extends SubscribingComponent {
}
private removeUrl(url: string) {
this.imagesUrls = this.imagesUrls.filter(u => u !== url)
this.imagesIds = this.imagesIds.filter(u => u !== url)
}
private getImagePath(id: string): string {
return `recipes/${this.recipe.id}/${id}`
}
}

View File

@ -126,7 +126,7 @@
<button
mat-raised-button
color="accent"
[disabled]="!hasSimdut(getMixMaterialFromDto(mixMaterial).material)"
[disabled]="!getMixMaterialFromDto(mixMaterial).material.hasSimdut"
(click)="openSimdut(getMixMaterialFromDto(mixMaterial))">
Fiche signalitique
</button>

View File

@ -1,5 +1,11 @@
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
import {Mix, MixMaterial, MixMaterialDto, mixMaterialsAsMixMaterialsDto, Recipe} from '../../../shared/model/recipe.model'
import {
Mix,
MixQuantity,
MixMaterialDto,
mixMaterialsToMixMaterialsDto,
Recipe
} from '../../../shared/model/recipe.model'
import {Subject} from 'rxjs'
import {SubscribingComponent} from '../../../shared/components/subscribing.component'
import {convertMixMaterialQuantity, UNIT_MILLILITER} from '../../../shared/units'
@ -37,7 +43,7 @@ export class MixTableComponent extends SubscribingComponent {
mixColumns = this.COLUMNS
units = UNIT_MILLILITER
mixMaterials: MixMaterialDto[] = []
hoveredMixMaterial: MixMaterial | null
hoveredMixMaterial: MixQuantity | null
// BPac printer
printer: PtouchPrinter | null
@ -60,7 +66,7 @@ export class MixTableComponent extends SubscribingComponent {
this.mixColumns = this.COLUMNS_EDIT
}
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix)
this.mixMaterials = mixMaterialsToMixMaterialsDto(this.mix)
this.subscribe(
this.units$,
@ -68,11 +74,7 @@ export class MixTableComponent extends SubscribingComponent {
)
}
hasSimdut(material: Material): boolean {
return material.simdutUrl != null
}
openSimdut(mixMaterial: MixMaterial) {
openSimdut(mixMaterial: MixQuantity) {
openSimdut(mixMaterial.material)
}
@ -102,8 +104,8 @@ export class MixTableComponent extends SubscribingComponent {
})
}
getMixMaterialFromDto(mixMaterialDto: MixMaterialDto): MixMaterial {
return this.mix.mixMaterials.find(m => m.material.id === mixMaterialDto.materialId)
getMixMaterialFromDto(mixMaterialDto: MixMaterialDto): MixQuantity {
return this.mix.mixQuantities.find(m => m.material.id === mixMaterialDto.materialId)
}
getMixMaterialQuantityRounded(mixMaterial: MixMaterialDto): number {
@ -121,7 +123,7 @@ export class MixTableComponent extends SubscribingComponent {
return totalQuantity
}
getCalculatedQuantityHtml(mixMaterial: MixMaterial, index: number): string {
getCalculatedQuantityHtml(mixMaterial: MixQuantity, index: number): string {
const totalQuantity = this.round(this.getTotalQuantity(index))
const addedQuantity = this.round(this.calculateQuantity(index))
return `<span class="mix-calculated-quantity">+${addedQuantity}</span> <span class="mix-calculated-quantity">(${totalQuantity})</span>`
@ -137,7 +139,7 @@ export class MixTableComponent extends SubscribingComponent {
}
async print() {
const base = this.mix.mixMaterials
const base = this.mix.mixQuantities
.map(ma => ma.material)
.filter(m => m.materialType.name === 'Base')[0]
if (!base) {
@ -191,12 +193,14 @@ export class MixTableComponent extends SubscribingComponent {
materialId: quantity.materialId,
quantity: this.calculateQuantity(index),
isPercents: quantity.isPercents,
position: quantity.position
position: quantity.position,
units: UNIT_MILLILITER,
isMixType: false // TODO
})
}
private convertQuantities(newUnit: string) {
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, this.units, newUnit))
this.mixMaterials.forEach(q => q.quantity = convertMixMaterialQuantity(q, newUnit))
this.units = newUnit
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,43 +1,47 @@
<div class="action-bar">
<mat-form-field>
<mat-label>Recherche</mat-label>
<input
matInput
type="text"
[(ngModel)]="searchQuery"
(keyup)="searchRecipes()"/>
<button
mat-button
*ngIf="searchQuery"
matSuffix
mat-icon-button
(click)="searchQuery=''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<div class="button-add">
<button *ngIf="hasEditPermission" mat-raised-button color="accent" routerLink="/color/add">Ajouter</button>
</div>
<cre-action-bar>
<cre-action-group>
<cre-input label="Recherche" [control]="searchControl"></cre-input>
</cre-action-group>
<cre-action-group>
<cre-accent-button *ngIf="hasEditPermission" routerLink="/color/add">Ajouter</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div *ngIf="!loading">
<cre-warning-alert *ngIf="companies.length === 0">
<p>Il n'y a actuellement aucune bannière enregistrée dans le système.</p>
<p *ngIf="hasCompanyEditPermission">Vous pouvez en créer une <b><a routerLink="/catalog/company/add">ici</a></b>.
</p>
</cre-warning-alert>
<cre-warning-alert *ngIf="companies.length > 0 && recipes.size === 0">
<p>Il n'y a actuellement aucune recette enregistrée dans le système.</p>
<p *ngIf="hasEditPermission">Vous pouvez en créer une <b><a routerLink="/color/add">ici</a></b>.</p>
</cre-warning-alert>
</div>
<mat-expansion-panel
class="table-title"
*ngFor="let companyRecipes of recipes"
[hidden]="isCompanyHidden(companyRecipes.recipes)"
*ngFor="let company of companies"
[hidden]="isCompanyHidden(company)"
[expanded]="panelForcedExpanded">
<mat-expansion-panel-header>
<mat-panel-title>
{{companyRecipes.company}}
{{company.name}}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: companyRecipes.recipes}"></ng-container>
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: recipes.get(company.id)}"></ng-container>
</mat-expansion-panel>
<ng-template
#recipeTableTemplate
let-recipes="recipes">
<table class="mx-auto" mat-table [dataSource]="recipes">
<cre-table
class="w-100"
[columns]="columns"
[data]="recipes"
[filter]="searchQuery"
[filterPredicate]="recipeFilterPredicate">
<!-- Recipe's info -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
@ -85,19 +89,16 @@
<!-- Buttons -->
<ng-container matColumnDef="buttonView">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe">
<button mat-flat-button color="accent" routerLink="/color/explore/{{recipe.id}}">Voir</button>
<td mat-cell *matCellDef="let recipe; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/explore/{{recipe.id}}">Voir</cre-accent-button>
</td>
</ng-container>
<ng-container matColumnDef="buttonEdit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe" [class.disabled]="!hasEditPermission">
<button mat-flat-button color="accent" routerLink="/color/edit/{{recipe.id}}">Modifier</button>
<td mat-cell *matCellDef="let recipe; let i = index" [class.disabled]="!hasEditPermission">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/edit/{{recipe.id}}">Modifier</cre-accent-button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let recipe; columns: tableCols" [hidden]="hiddenRecipes[recipe.id]"></tr>
</table>
</cre-table>
</ng-template>

View File

@ -0,0 +1,110 @@
import {ChangeDetectorRef, Component} from '@angular/core'
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
import {Company} from '../shared/model/company.model'
import {getRecipeLuma, Recipe, recipeMatchesFilter} from '../shared/model/recipe.model'
import {CompanyService} from '../company/service/company.service'
import {RecipeService} from './services/recipe.service'
import {AccountService} from '../accounts/services/account.service'
import {ConfigService} from '../shared/service/config.service'
import {AppState} from '../shared/app-state'
import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {Config} from '../shared/model/config.model'
import {Permission} from '../shared/model/user'
import {FormControl} from '@angular/forms'
@Component({
selector: 'cre-recipe-list',
templateUrl: 'list.html',
styleUrls: ['recipes.sass']
})
export class RecipeList extends ErrorHandlingComponent {
companies: Company[] = []
recipes: Map<number, Recipe[]> = new Map<number, Recipe[]>()
columns = ['name', 'description', 'color', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
panelForcedExpanded = false
searchControl: FormControl
searchQuery = ''
recipeFilterPredicate = recipeMatchesFilter
constructor(
private companyService: CompanyService,
private recipeService: RecipeService,
private accountService: AccountService,
private configService: ConfigService,
private cdRef: ChangeDetectorRef,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.appState.title = 'Explorateur'
// Navigate to configs if server is in emergency mode
this.subscribe(
this.configService.get(Config.EMERGENCY_MODE),
config => {
if (config.content == 'true') {
this.urlUtils.navigateTo('/admin/config/')
}
}
)
this.fetchCompanies()
this.fetchRecipes()
this.searchControl = new FormControl('')
this.subscribe(
this.searchControl.valueChanges,
value => {
this.searchQuery = value
if (value.length > 0 && !this.panelForcedExpanded) {
this.panelForcedExpanded = true
this.cdRef.detectChanges()
}
}
)
}
private fetchCompanies() {
this.subscribe(
this.companyService.all,
companies => this.companies = companies
)
}
private fetchRecipes() {
this.subscribe(
this.recipeService.allByCompany,
recipes => this.recipes = recipes,
true
)
}
isCompanyHidden(company: Company): boolean {
const companyRecipes = this.recipes.get(company.id)
return !(companyRecipes && companyRecipes.length >= 0) ||
this.searchQuery && this.searchQuery.length > 0 &&
!companyRecipes.some(recipe => this.recipeFilterPredicate(recipe, this.searchQuery))
}
isLight(recipe: Recipe): boolean {
return getRecipeLuma(recipe) > 200
}
get hasEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_RECIPES)
}
get hasCompanyEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
}

View File

@ -0,0 +1,20 @@
<ng-container *ngIf="recipe">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/edit/{{recipe.id}}">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-accent-button [disabled]="!form.valid" (click)="submit(form.formValues)">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<cre-mix-form
#form
[recipe]="recipe"
[materialTypes]="materialTypes$"
[materials]="materials$">
<cre-form-title>
Ajouter un mélange à la couleur {{recipe.company.name}} - {{recipe.name}}
</cre-form-title>
</cre-mix-form>
</ng-container>

View File

@ -0,0 +1,28 @@
<ng-container *ngIf="recipe && mix">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/edit/{{recipe.id}}">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-warn-button (click)="deleteConfirmBox.show()">Supprimer</cre-warn-button>
<cre-accent-button [disabled]="!form.valid" (click)="submit(form.formValues)">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<cre-mix-form
#form
[recipe]="recipe"
[mix]="mix"
[materialTypes]="materialTypes$"
[materials]="materials$">
<cre-form-title>
Modification du mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}}
</cre-form-title>
</cre-mix-form>
<cre-confirm-box
#deleteConfirmBox
message="Voulez-vous vraiment supprimer le mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}}?"
(confirm)="delete()">
</cre-confirm-box>
</ng-container>

View File

@ -0,0 +1,13 @@
<cre-mix-info-form
[recipe]="recipe"
[mix]="mix"
[materialTypes]="materialTypes">
<cre-form-title>
<ng-content select="cre-form-title"></ng-content>
</cre-form-title>
</cre-mix-info-form>
<cre-mix-materials-form
[materials]="materials"
[mix]="mix">
</cre-mix-materials-form>

View File

@ -0,0 +1,13 @@
<cre-form [formControls]="controls" class="mx-auto">
<!-- <cre-form-title>-->
<!-- Ajouter un mélange à la couleur {{recipe.company.name}} - {{recipe.name}}-->
<!-- </cre-form-title>-->
<cre-form-title>
<ng-content select="cre-form-title"></ng-content>
</cre-form-title>
<cre-form-content>
<cre-input [control]="controls.name" label="Name" icon="form-textbox"></cre-input>
<cre-select [control]="controls.materialType" label="Type de produit" [entries]="materialTypeEntries"></cre-select>
</cre-form-content>
</cre-form>

View File

@ -0,0 +1,5 @@
<cre-combo-box
*ngIf="entries"
[control]="control"
[entries]="entries">
</cre-combo-box>

View File

@ -0,0 +1,76 @@
<div class="mt-5">
<cre-warning-alert *ngIf="materialCount <= 0">
<p>Il n'y a actuellement aucun produit enregistré dans le système.</p>
<p *ngIf="hasMaterialEditPermission">Vous pouvez en créer un <b><a routerLink="/catalog/material/add">ici</a></b>.
</p>
</cre-warning-alert>
<cre-table [hidden]="materialCount <= 0" class="mx-auto" [data]="mixMaterials" [columns]="columns">
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.position}}</td>
</ng-container>
<ng-container matColumnDef="positionButtons">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-table-position-buttons
[position]="mixMaterial.position"
[min]="1"
[max]="mixMaterials.length"
[disableDecreaseButton]="isDecreasePositionButtonDisabled(mixMaterial)"
[disableIncreaseButton]="isIncreasePositionButtonDisabled(mixMaterial)"
(positionChange)="updatePosition(mixMaterial, $event)">
</cre-table-position-buttons>
</td>
</ng-container>
<ng-container matColumnDef="material">
<th mat-header-cell *matHeaderCellDef>Produit</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-mix-materials-form-combo-box
[mixMaterial]="mixMaterial"
[mix]="mix"
[mixMaterials]="mixMaterials"
[control]="getControls(mixMaterial.position).materialId"
[materials]="allMaterialsValues"
[position]="mixMaterial.position">
</cre-mix-materials-form-combo-box>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-input [control]="getControls(mixMaterial.position).quantity" type="number"></cre-input>
</td>
</ng-container>
<ng-container matColumnDef="units">
<th mat-header-cell *matHeaderCellDef>Unités</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-unit-selector
*ngIf="!areUnitsPercents(mixMaterial)"
[showLabel]="false"
[short]="true"
[control]="getControls(mixMaterial.position).units">
</cre-unit-selector>
<ng-container *ngIf="areUnitsPercents(mixMaterial)">
%
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="endButton">
<th mat-header-cell *matHeaderCellDef>
<cre-accent-button (click)="addRow()" [disabled]="materialCount - mixMaterials.length <= 0">Ajouter
</cre-accent-button>
</th>
<td mat-cell *matCellDef="let mixMaterial">
<cre-warn-button (click)="removeRow(mixMaterial)" [disabled]="mixMaterials.length === 1">Retirer
</cre-warn-button>
</td>
</ng-container>
</cre-table>
</div>

View File

@ -0,0 +1,278 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewChildren
} from '@angular/core'
import {CreTable} from '../../shared/components/tables/tables'
import {Mix, MixMaterialDto, mixMaterialsToMixMaterialsDto, sortMixMaterialsDto} from '../../shared/model/recipe.model'
import {Observable, Subject} from 'rxjs'
import {Material, materialComparator} from '../../shared/model/material.model'
import {FormControl, Validators} from '@angular/forms'
import {takeUntil} from 'rxjs/operators'
import {CreComboBoxComponent, CreInputEntry} from '../../shared/components/inputs/inputs'
import {AccountService} from '../../accounts/services/account.service'
import {Permission} from '../../shared/model/user'
import {UNIT_MILLILITER} from '../../shared/units'
@Component({
selector: 'cre-mix-materials-form-combo-box',
templateUrl: 'materials-form-combo-box.html'
})
export class MixMaterialsFormComboBox implements OnInit {
@ViewChild(CreComboBoxComponent) comboBox: CreComboBoxComponent
@Input() mixMaterial: MixMaterialDto
@Input() mix: Mix | null
@Input() mixMaterials: MixMaterialDto[]
@Input() control: FormControl
@Input() materials: Material[]
@Input() position: number
entries: CreInputEntry[]
ngOnInit() {
this.entries = this.filterMaterials()
}
updateEntries() {
this.entries = this.filterMaterials()
this.comboBox.reloadEntries()
}
private filterMaterials(): CreInputEntry[] {
return this.materials
.filter(material => {
// Prevent use of percents in first position
if (material.materialType.usePercentages && this.mixMaterial.position <= 1) {
return false
}
if (this.mixMaterial.materialId === material.id) {
return true
}
return this.mixMaterials.filter(x => x.materialId === material.id).length <= 0
})
.sort(materialComparator)
.map(this.materialAsInputEntry)
}
private materialAsInputEntry(material: Material): CreInputEntry {
return new CreInputEntry(
material.id,
material.name,
material.materialType.prefix ? `[${material.materialType.prefix}] ${material.name}` : material.name,
{
bold: material.isMixType
}
)
}
}
@Component({
selector: 'cre-mix-materials-form',
templateUrl: 'materials-form.html'
})
export class MixMaterialsForm implements AfterViewInit, OnDestroy {
@ViewChild(CreTable) table: CreTable<MixMaterialDto>
@ViewChildren(MixMaterialsFormComboBox) comboBoxes: MixMaterialsFormComboBox[]
@Input() materials: Observable<Material[]>
@Input() mix: Mix | null
mixMaterials: MixMaterialDto[] = []
columns = ['position', 'positionButtons', 'material', 'quantity', 'units', 'endButton']
allMaterials = new Map<number, Material>();
allMaterialsValues = [];
private _controls: ControlsByPosition[] = []
private _destroy$ = new Subject<boolean>()
constructor(
private accountService: AccountService,
private cdRef: ChangeDetectorRef
) {
}
ngAfterViewInit() {
this.materials.subscribe({
next: materials => {
this.allMaterials.clear()
this.allMaterialsValues = materials
materials.forEach(m => this.allMaterials.set(m.id, m))
if (!this.mix) {
this.addRow()
} else {
mixMaterialsToMixMaterialsDto(this.mix).forEach(x => this.insertRow(x))
}
this.table.renderRows()
this.cdRef.detectChanges()
}
})
}
ngOnDestroy() {
this._destroy$.next(true)
this._destroy$.complete()
}
addRow() {
const mixMaterial = new MixMaterialDto(null, 0, false, this.nextPosition, UNIT_MILLILITER, false)
this.insertRow(mixMaterial)
this.table.renderRows()
}
insertRow(mixMaterial: MixMaterialDto) {
const materialIdControl = new FormControl(mixMaterial.materialId, Validators.required)
const quantityControl = new FormControl(mixMaterial.quantity, Validators.required)
const unitsControl = new FormControl(mixMaterial.units, Validators.required)
materialIdControl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe({
next: materialId => {
mixMaterial.materialId = materialId
this.refreshAvailableMaterials()
this.cdRef.detectChanges()
}
})
this.mixMaterials.push(mixMaterial)
this._controls.push({
position: mixMaterial.position,
controls: {
materialId: materialIdControl,
quantity: quantityControl,
units: unitsControl
}
})
}
removeRow(mixMaterial: MixMaterialDto) {
this.mixMaterials = this.mixMaterials.filter(x => x.position !== mixMaterial.position)
this._controls = this._controls.filter(x => x.position !== mixMaterial.position)
for (let position = mixMaterial.position + 1; position < this.mixMaterials.length; position++) {
this.updatePosition(this.getMixMaterialByPosition(position), position - 1, false)
}
}
updatePosition(mixMaterial: MixMaterialDto, newPosition: number, switchPositions = true) {
const currentPosition = mixMaterial.position
const currentControls = this.getControlsByPosition(currentPosition)
// Update before current to prevent position conflicts
if (switchPositions) {
this.updatePosition(this.getMixMaterialByPosition(newPosition), currentPosition, false)
}
mixMaterial.position = newPosition
currentControls.position = newPosition
this.sortTable()
this.refreshAvailableMaterials()
}
getControls(position: number): MixMaterialControls {
return this.getControlsByPosition(position).controls
}
areUnitsPercents(mixMaterial: MixMaterialDto): boolean {
if (!mixMaterial) {
return false
}
return mixMaterial.materialId ? this.allMaterials.get(mixMaterial.materialId)?.materialType.usePercentages : false
}
isDecreasePositionButtonDisabled(mixMaterial: MixMaterialDto): boolean {
return mixMaterial.position <= 2 && this.areUnitsPercents(mixMaterial)
}
isIncreasePositionButtonDisabled(mixMaterial: MixMaterialDto): boolean {
if (mixMaterial.position === this.mixMaterials.length) {
return true
}
if (mixMaterial.position > 1) {
return false
}
const nextMixMaterial = this.getMixMaterialByPosition(mixMaterial.position + 1)
return this.areUnitsPercents(nextMixMaterial)
}
get hasMaterialEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_MATERIALS)
}
get materialCount(): number {
return this.allMaterials.size
}
get updatedMixMaterials(): MixMaterialDto[] {
const updatedMixMaterials: MixMaterialDto[] = []
this.mixMaterials.forEach(mixMaterial => {
const controls = this.getControlsByPosition(mixMaterial.position).controls
const materialId = controls.materialId.value;
const material = this.allMaterials.get(materialId)
updatedMixMaterials.push({
...mixMaterial,
materialId,
quantity: controls.quantity.value,
units: controls.units.value,
isPercents: material.materialType.usePercentages,
isMixType: material.isMixType
})
})
return updatedMixMaterials
}
get valid(): boolean {
return this._controls
.map(controls => controls.controls)
.map(controls => [controls.materialId, controls.quantity])
.flatMap(controls => controls)
.every(control => control.valid)
}
private get nextPosition(): number {
return this.mixMaterials.length + 1
}
private getMixMaterialByPosition(position: number): MixMaterialDto {
return this.mixMaterials.filter(x => x.position === position)[0]
}
private getControlsByPosition(position: number): ControlsByPosition {
return this._controls.filter(control => control.position === position)[0]
}
private refreshAvailableMaterials() {
this.comboBoxes.forEach(x => x.updateEntries())
}
private sortTable() {
this.mixMaterials = sortMixMaterialsDto(this.mixMaterials)
this.table.renderRows()
}
}
interface MixMaterialControls {
materialId: FormControl
quantity: FormControl
units: FormControl
}
interface ControlsByPosition {
position: number
controls: MixMaterialControls
}

View File

@ -0,0 +1,190 @@
import {Component, Directive, Input, OnInit, ViewChild} from '@angular/core'
import {SubscribingComponent} from '../../shared/components/subscribing.component'
import {Mix, Recipe} from '../../shared/model/recipe.model'
import {ErrorService} from '../../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {RecipeService} from '../services/recipe.service'
import {FormControl, Validators} from '@angular/forms'
import {Observable} from 'rxjs'
import {MaterialType} from '../../shared/model/materialtype.model'
import {MaterialTypeService} from '../../material-type/service/material-type.service'
import {CreInputEntry} from '../../shared/components/inputs/inputs'
import {map} from 'rxjs/operators'
import {Material} from '../../shared/model/material.model'
import {MaterialService} from '../../material/service/material.service'
import {CreForm} from '../../shared/components/forms/forms'
import {MixMaterialsForm} from './materials-form'
import {MixSaveDto, MixService, MixUpdateDto} from '../services/mix.service'
@Directive()
abstract class _BaseMixPage extends SubscribingComponent {
materialTypes$ = this.materialTypeService.all
materials$: Observable<Material[]>
private _recipe: Recipe | null
constructor(
protected mixService: MixService,
private recipeService: RecipeService,
private materialTypeService: MaterialTypeService,
protected materialService: MaterialService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
this.fetchRecipe()
}
private fetchRecipe() {
const recipeId = this.urlUtils.parseIntUrlParam('recipeId')
this.subscribe(
this.recipeService.getById(recipeId),
recipe => this.recipe = recipe
)
}
set recipe(recipe: Recipe) {
this._recipe = recipe
this.materials$ = this.fetchMaterials(recipe.id)
}
get recipe(): Recipe {
return this._recipe
}
protected abstract fetchMaterials(recipeId: number): Observable<Material[]>
abstract submit(dto: MixSaveDto)
}
@Component({
selector: 'cre-mix-add',
templateUrl: 'add.html'
})
export class MixAdd extends _BaseMixPage {
protected fetchMaterials(recipeId: number): Observable<Material[]> {
return this.materialService.getAllForMixCreation(recipeId)
}
submit(dto: MixSaveDto) {
this.subscribeAndNavigate(
this.mixService.saveDto(dto),
`/color/edit/${this.recipe.id}`
)
}
}
@Component({
selector: 'cre-mix-edit',
templateUrl: 'edit.html'
})
export class MixEdit extends _BaseMixPage {
mix: Mix
private mixId: number
ngOnInit() {
super.ngOnInit()
this.mixId = this.urlUtils.parseIntUrlParam('id')
this.fetchMix()
}
private fetchMix() {
this.subscribe(
this.mixService.getById(this.mixId),
mix => this.mix = mix
)
}
protected fetchMaterials(recipeId: number): Observable<Material[]> {
return this.materialService.getAllForMixUpdate(this.mixId)
}
submit(dto: MixSaveDto) {
this.subscribeAndNavigate(
this.mixService.updateDto({...dto, id: this.mix.id}),
`/color/edit/${this.recipe.id}`
)
}
delete() {
this.subscribeAndNavigate(
this.mixService.delete(this.mixId),
'/color/edit/' + this.recipe.id
)
}
}
@Component({
selector: 'cre-mix-info-form',
templateUrl: 'info-form.html'
})
export class MixInfoForm implements OnInit {
@ViewChild(CreForm) form: CreForm
@Input() recipe: Recipe
@Input() mix: Mix | null
@Input() materialTypes: Observable<MaterialType[]>
materialTypeEntries: Observable<CreInputEntry[]>
controls: any
ngOnInit() {
this.materialTypeEntries = this.materialTypes.pipe(
map(materialTypes => {
return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))
})
)
this.controls = {
name: new FormControl(this.mix?.mixType.name, Validators.required),
materialType: new FormControl(this.mix?.mixType.materialType.id, Validators.required)
}
}
get mixName(): string {
return this.controls.name.value
}
get mixMaterialTypeId(): number {
return this.controls.materialType.value
}
get valid(): boolean {
return this.form.valid
}
}
@Component({
selector: 'cre-mix-form',
templateUrl: 'form.html'
})
export class MixForm {
@ViewChild(MixInfoForm) infoForm: MixInfoForm
@ViewChild(MixMaterialsForm) mixMaterialsForm: MixMaterialsForm
@Input() recipe: Recipe
@Input() mix: Mix | null
@Input() materialTypes: Observable<MaterialType[]>
@Input() materials: Observable<Material[]>
get formValues(): MixSaveDto {
return {
name: this.infoForm.mixName,
recipeId: this.recipe.id,
materialTypeId: this.infoForm.mixMaterialTypeId,
mixQuantities: this.mixMaterialsForm.updatedMixMaterials
}
}
get valid(): boolean {
return this.infoForm?.valid && this.mixMaterialsForm?.valid
}
}

View File

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

View File

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

View File

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

View File

@ -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<Recipe>();
controls: any
companyEntries$: Observable<CreInputEntry[]>
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<string>()
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<number, RecipeStep[]>): boolean {
return !anyMap(steps, (groupId, steps) => !!steps.find(s => s.position === 0))
}
}

View File

@ -21,8 +21,17 @@ export class MixService {
return this.api.get<Mix>(`/recipe/mix/${id}`)
}
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
saveDto(dto: MixSaveDto): Observable<void> {
return this.saveWithUnits(
dto.name,
dto.recipeId,
dto.materialTypeId,
dto.mixQuantities,
)
}
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials))
}
save(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
@ -30,14 +39,23 @@ export class MixService {
name,
recipeId,
materialTypeId,
mixMaterials: []
mixQuantities: []
}
this.appendMixMaterialsToBody(mixMaterials, body)
return this.api.post('/recipe/mix', body)
}
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
updateDto(dto: MixUpdateDto): Observable<void> {
return this.updateWithUnits(
dto.id,
dto.name,
dto.materialTypeId,
dto.mixQuantities
)
}
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials))
}
update(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
@ -45,7 +63,7 @@ export class MixService {
id,
name,
materialTypeId,
mixMaterials: []
mixQuantities: []
}
this.appendMixMaterialsToBody(mixMaterials, body)
@ -56,21 +74,37 @@ 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) {
mixMaterials.filter(m => m.materialId != null && m.quantity != null).forEach(m => {
body.mixMaterials.push({
body.mixQuantities.push({
materialId: m.materialId,
quantity: m.quantity,
position: m.position
position: m.position,
isMixType: m.isMixType
})
})
}
}
export interface MixSaveDto {
name: string
recipeId: number
materialTypeId: number
mixQuantities: MixMaterialDto[]
}
export interface MixUpdateDto {
id: number
name: string
materialTypeId: number
mixQuantities: MixMaterialDto[]
}

View File

@ -12,10 +12,10 @@ export class RecipeImageService {
) {
}
save(image: File, recipeId: number): Observable<Recipe> {
save(image: File, recipeId: number): Observable<string> {
const body = new FormData()
body.append('image', image)
return this.api.put<Recipe>(`/recipe/${recipeId}/image`, body)
return this.api.put<string>(`/recipe/${recipeId}/image`, body)
}
delete(url: string, recipeId: number): Observable<void> {

View File

@ -21,16 +21,16 @@ export class RecipeService {
return this.api.get<Recipe[]>(`/recipe?name=${name}`)
}
get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> {
get allByCompany(): Observable<Map<number, Recipe[]>> {
return this.all.pipe(map(recipes => {
const mapped = []
const map = new Map<number, Recipe[]>()
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>(`/recipe/${id}`)
}
save(name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, companyId: number): Observable<Recipe> {
const body = {name, description, color, gloss, sample, remark, companyId}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
save(recipe: Recipe): Observable<Recipe> {
const body = {
...recipe,
companyId: recipe.company
}
return this.api.post<Recipe>('/recipe', body)
}
update(id: number, name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, steps: Map<number, RecipeStep[]>) {
const body = {id, name, description, color, gloss, sample, remark, steps: []}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
update(recipe: Recipe, steps: Map<number, RecipeStep[]>) {
const body = {
...recipe,
steps: []
}
steps.forEach((groupSteps, groupId) => {
@ -85,4 +84,8 @@ export class RecipeService {
delete(id: number): Observable<void> {
return this.api.delete<void>(`/recipe/${id}`)
}
getImagesIds(id: number): Observable<string[]> {
return this.api.get<string[]>(`/recipe/${id}/image`)
}
}

View File

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

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