Compare commits

..

No commits in common. "master" and "release/2.0.0b1" have entirely different histories.

156 changed files with 2046 additions and 2720 deletions

View File

@ -1,13 +1,12 @@
---
global-variables:
release: &release ${DRONE_TAG}
release: &release ${DRONE_BRANCH##**/}
environment: &environment
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/frontend
CRE_REGISTRY_IMAGE: registry.fyloz.dev:5443/colorrecipesexplorer/frontend
CRE_PORT: 9102
CRE_RELEASE: *release
alpine-image: &alpine-image alpine:latest
docker-registry: &docker-registry registry.fyloz.dev
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/frontend
docker-registry-repo: &docker-registry-repo registry.fyloz.dev:5443/colorrecipesexplorer/frontend
kind: pipeline
name: default
@ -22,9 +21,6 @@ steps:
- echo -n "latest" > .tags
when:
branch: develop
event:
exclude:
- pull_request
- name: set-docker-tags-release
image: *alpine-image
@ -33,40 +29,18 @@ steps:
commands:
- echo -n "latest-release,$CRE_RELEASE" > .tags
when:
event:
- tag
branch: release/**
- name: containerize-dev
- name: containerize
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
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
branch:
- develop
- release/**
- name: deploy
image: alpine:latest
@ -96,5 +70,10 @@ 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:
event:
- tag
branch: release/**
trigger:
branch:
- develop
- release/**
- master

View File

@ -1,93 +0,0 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json",
"e2e/tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/component-class-suffix": "off",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "cre",
"style": "kebab-case"
}
],
"@angular-eslint/directive-class-suffix": "off",
"@angular-eslint/directive-selector": [
"off",
{
"type": "attribute",
"prefix": "cre",
"style": "camelCase"
}
],
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/member-delimiter-style": [
"off",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/semi": [
"off",
"never"
],
"arrow-parens": [
"off",
"always"
],
"eqeqeq": [
"off",
"always"
],
"import/order": "off",
"max-len": [
"off",
{
"code": 140
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,11 +1,19 @@
version: "3.1"
services:
cre.backend:
database:
image: mysql
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- "3306:3306"
backend:
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database/cre"
cre_database_url: "mysql://database:3306/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 1
@ -15,14 +23,6 @@ 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,66 +3,53 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.conf.json",
"start": "ng serve --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": "~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",
"@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",
"bootstrap": "^4.5.2",
"copy-webpack-plugin": "^10.0.0",
"jwt-decode": "^3.1.2",
"copy-webpack-plugin": "^6.2.1",
"js-joda": "^1.11.0",
"material-design-icons": "^3.0.1",
"ngx-material-file-input": "^2.1.1",
"rxjs": "^7.4.0",
"tslib": "^2.3.1",
"zone.js": "~0.11.4"
"rxjs": "~6.5.4",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^12.2.13",
"@angular-eslint/builder": "4.3.0",
"@angular-eslint/eslint-plugin": "4.3.0",
"@angular-eslint/eslint-plugin-template": "4.3.0",
"@angular-eslint/schematics": "4.3.0",
"@angular-eslint/template-parser": "4.3.0",
"@angular/cli": "^12.2.13",
"@angular/compiler-cli": "~12.2.14",
"@angular/language-service": "~12.2.14",
"@angular-devkit/build-angular": "^0.1102.9",
"@angular/cli": "^11.2.11",
"@angular/compiler-cli": "~11.2.10",
"@angular/language-service": "~11.2.10",
"@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": "^8.3.0",
"eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-prefer-arrow": "latest",
"jasmine-core": "^3.10.1",
"jasmine-spec-reporter": "^7.0.0",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.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": "^10.4.0",
"typescript": "~4.3.5"
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.7"
}
}

View File

@ -18,7 +18,3 @@ $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/recipes/recipes.module').then(m => m.RecipesModule)
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule)
}, {
path: 'account',
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)

View File

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

View File

@ -1,17 +1,10 @@
import {NgModule} from '@angular/core'
import {RouterModule, Routes} from '@angular/router'
import {Login, Logout} from './accounts'
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
const routes: Routes = [{
path: 'login',
component: Login
}, {
path: 'logout',
component: Logout
}, {
path: '',
redirectTo: 'login'
}]
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'}];
@NgModule({
imports: [RouterModule.forChild(routes)],

View File

@ -1,22 +1,18 @@
import {NgModule} from '@angular/core'
import {NgModule} from '@angular/core';
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'
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";
@NgModule({
declarations: [
Login,
Logout
],
declarations: [LoginComponent, LogoutComponent],
imports: [
SharedModule,
AccountsRoutingModule,
CreInputsModule,
CreButtonsModule,
]
})
export class AccountsModule {

View File

@ -1,91 +0,0 @@
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

@ -1,28 +0,0 @@
<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

@ -0,0 +1,36 @@
<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

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

View File

@ -0,0 +1,53 @@
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

@ -0,0 +1,28 @@
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 {Observable, Subject} from 'rxjs'
import {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 {Permission, User} from '../../shared/model/user'
import {User, Permission} 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,7 +20,6 @@ export class AccountService implements OnDestroy {
private http: HttpClient,
private api: ApiService,
private appState: AppState,
private jwtService: JwtService,
private errorService: ErrorService,
private alertService: AlertService
) {
@ -31,16 +30,20 @@ export class AccountService implements OnDestroy {
this.destroy$.complete()
}
isLoggedIn(): boolean {
return this.appState.isAuthenticated
}
checkAuthenticationStatus() {
if (!this.appState.isAuthenticated) {
if (!this.appState.authenticatedUser) {
// Try to get current default group user
this.http.get<User>(`${environment.apiUrl}/user/group/currentuser`, {withCredentials: true})
this.http.get<User>(`${environment.apiUrl}/user/current`, {withCredentials: true})
.pipe(
take(1),
takeUntil(this.destroy$),
).subscribe(
{
next: user => this.appState.authenticateGroupUser(user),
next: user => this.appState.authenticatedUser = user,
error: err => {
if (err.status === 404 || err.status === 403) {
console.warn('No default user is defined on this computer')
@ -52,74 +55,67 @@ export class AccountService implements OnDestroy {
}
}
login(userId: number, password: string): Observable<any> {
const subject = new Subject<void>()
this.http.post<any>(`${environment.apiUrl}/login`, {id: userId, password}, {
login(id: number, password: string, success: () => void) {
const loginForm = {id, password}
globalLoadingWheel.show()
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
withCredentials: true,
observe: 'response' as 'body'
}).pipe(
take(1),
takeUntil(this.destroy$)
).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)
})
.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)
}
}
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(
logout(success: () => void) {
this.api.get<void>('/logout', true).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()
)
.subscribe({
next: () => {
this.appState.resetAuthenticatedUser()
this.checkAuthenticationStatus()
success()
},
error: err => this.errorService.handleError(err)
})
}
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

@ -1,13 +0,0 @@
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

@ -0,0 +1,39 @@
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

@ -0,0 +1,38 @@
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,17 +1,15 @@
<mat-card *ngIf="editionMode || imagesIds">
<mat-card *ngIf="editionMode || imagesUrls">
<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">
<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 *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
<div class="image-wrapper">
<img [src]="getImageUrl(imageId)" width="300px"/>
<img [src]="imageUrl" 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(imageId)">Afficher</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageId)">Retirer</button>
<button mat-raised-button color="primary" (click)="openImage(imageUrl)">Afficher</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageUrl)">Retirer</button>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,136 @@
<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

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

View File

@ -0,0 +1,220 @@
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

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

View File

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

View File

@ -3,8 +3,6 @@
<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 *ngIf="steps.length > 0">
<mat-list>
<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)="onUnitChange($event.value)">
<mat-select [value]="unit" (selectionChange)="unitChange.emit($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,28 +1,17 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'
import {Component, EventEmitter, Input, 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 implements OnInit {
export class UnitSelectorComponent {
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,6 @@
<cre-entity-add
title="Création d'une recette"
backButtonLink="/color/list"
[formFields]="formFields"
(submit)="submit($event)">
</cre-entity-add>

View File

@ -0,0 +1,123 @@
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

@ -0,0 +1,68 @@
<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

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

View File

@ -0,0 +1,186 @@
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

@ -0,0 +1,86 @@
<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

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

View File

@ -1,34 +1,27 @@
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 {FormControl} from '@angular/forms';
import {map} from 'rxjs/operators';
import {CreInputEntry} from '../shared/components/inputs/inputs';
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'
@Component({
selector: 'cre-recipe-explore',
templateUrl: './explore.html',
styleUrls: ['./recipes.sass']
selector: 'cre-explore',
templateUrl: './explore.component.html',
styleUrls: ['./explore.component.sass']
})
export class CreRecipeExplore extends ErrorHandlingComponent {
export class ExploreComponent extends ErrorHandlingComponent {
recipe: Recipe | null
groups$ = this.groupService.all
deductErrorBody = {}
units$ = new Subject<string>()
selectedGroupId: number | null
@ -40,12 +33,6 @@ export class CreRecipeExplore 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')
@ -55,9 +42,6 @@ export class CreRecipeExplore 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,
@ -78,30 +62,18 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
this.selectedGroupId = this.loggedInUserGroupId
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'))
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
this.subscribeEntityById(
this.recipeService,
recipeId,
recipe => this.recipe = recipe
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')
}
}
)
}
@ -143,7 +115,7 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
}
deductMix() {
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === this.deductedMixId)[0].mixQuantities[0]
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === this.deductedMixId)[0].mixMaterials[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)
@ -156,24 +128,11 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
subscribeDeductMix(observable: Observable<any>) {
this.subscribe(
observable,
() => this.alertService.pushSuccess('Les quantités ont été déduites de l\'inventaire'),
() => this.alertService.pushSuccess('Les quantités 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
}
@ -182,7 +141,11 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
if (!this.groupsNote.has(this.selectedGroupId)) {
this.groupsNote.set(this.selectedGroupId, recipeNoteForGroupId(this.recipe, this.selectedGroupId))
}
return this.groupsNote.get(this.selectedGroupId) ?? this._notePlaceholder
return this.groupsNote.get(this.selectedGroupId)
}
set selectedGroupNote(value: string) {
this.groupsNote.set(this.selectedGroupId, value)
}
get canEditRecipesPublicData(): boolean {
@ -197,9 +160,7 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
})
this.groupsNote.forEach((content, groupId) => {
if (content) {
updatedNotes.set(groupId, content)
}
updatedNotes.set(groupId, content)
})
return updatedNotes

View File

@ -1,47 +1,43 @@
<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 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>
</div>
<mat-expansion-panel
class="table-title"
*ngFor="let company of companies"
[hidden]="isCompanyHidden(company)"
*ngFor="let companyRecipes of recipes"
[hidden]="isCompanyHidden(companyRecipes.recipes)"
[expanded]="panelForcedExpanded">
<mat-expansion-panel-header>
<mat-panel-title>
{{company.name}}
{{companyRecipes.company}}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: recipes.get(company.id)}"></ng-container>
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: companyRecipes.recipes}"></ng-container>
</mat-expansion-panel>
<ng-template
#recipeTableTemplate
let-recipes="recipes">
<cre-table
class="w-100"
[columns]="columns"
[data]="recipes"
[filter]="searchQuery"
[filterPredicate]="recipeFilterPredicate">
<table class="mx-auto" mat-table [dataSource]="recipes">
<!-- Recipe's info -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
@ -89,16 +85,19 @@
<!-- Buttons -->
<ng-container matColumnDef="buttonView">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/explore/{{recipe.id}}">Voir</cre-accent-button>
<td mat-cell *matCellDef="let recipe">
<button mat-flat-button color="accent" routerLink="/color/explore/{{recipe.id}}">Voir</button>
</td>
</ng-container>
<ng-container matColumnDef="buttonEdit">
<th mat-header-cell *matHeaderCellDef></th>
<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 mat-cell *matCellDef="let recipe" [class.disabled]="!hasEditPermission">
<button mat-flat-button color="accent" routerLink="/color/edit/{{recipe.id}}">Modifier</button>
</td>
</ng-container>
</cre-table>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let recipe; columns: tableCols" [hidden]="hiddenRecipes[recipe.id]"></tr>
</table>
</ng-template>

View File

@ -0,0 +1,9 @@
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

@ -0,0 +1,91 @@
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

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

View File

@ -0,0 +1,45 @@
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

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

View File

@ -0,0 +1,59 @@
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

@ -21,17 +21,8 @@ export class MixService {
return this.api.get<Mix>(`/recipe/mix/${id}`)
}
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))
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> {
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
}
save(name: string, recipeId: number, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
@ -39,23 +30,14 @@ export class MixService {
name,
recipeId,
materialTypeId,
mixQuantities: []
mixMaterials: []
}
this.appendMixMaterialsToBody(mixMaterials, body)
return this.api.post('/recipe/mix', body)
}
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))
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[], units: string): Observable<void> {
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
}
update(id: number, name: string, materialTypeId: number, mixMaterials: MixMaterialDto[]): Observable<void> {
@ -63,7 +45,7 @@ export class MixService {
id,
name,
materialTypeId,
mixQuantities: []
mixMaterials: []
}
this.appendMixMaterialsToBody(mixMaterials, body)
@ -74,37 +56,21 @@ export class MixService {
return this.api.delete(`/recipe/mix/${id}`)
}
private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[]): MixMaterialDto[] {
return mixMaterials.map(mixMaterial => ({
...mixMaterial,
quantity: convertMixMaterialQuantity(mixMaterial, UNIT_MILLILITER),
units: UNIT_MILLILITER
}))
private convertMixMaterialsToMl(mixMaterials: MixMaterialDto[], units: string): MixMaterialDto[] {
return mixMaterials.map(m => {
m.quantity = convertMixMaterialQuantity(m, units, UNIT_MILLILITER)
return m
})
}
private appendMixMaterialsToBody(mixMaterials: MixMaterialDto[], body: any) {
mixMaterials.filter(m => m.materialId != null && m.quantity != null).forEach(m => {
body.mixQuantities.push({
body.mixMaterials.push({
materialId: m.materialId,
quantity: m.quantity,
position: m.position,
isMixType: m.isMixType
position: m.position
})
})
}
}
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<string> {
save(image: File, recipeId: number): Observable<Recipe> {
const body = new FormData()
body.append('image', image)
return this.api.put<string>(`/recipe/${recipeId}/image`, body)
return this.api.put<Recipe>(`/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 allByCompany(): Observable<Map<number, Recipe[]>> {
get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> {
return this.all.pipe(map(recipes => {
const map = new Map<number, Recipe[]>()
const mapped = []
recipes.forEach(r => {
if (!map.has(r.company.id)) {
map.set(r.company.id, [])
if (!mapped[r.company.id]) {
mapped[r.company.id] = {company: r.company.name, recipes: []}
}
map.get(r.company.id).push(r)
mapped[r.company.id].recipes.push(r)
})
return map
return mapped.filter(e => e != null) // Filter to remove empty elements in the array that appears for some reason
}))
}
@ -38,19 +38,20 @@ export class RecipeService {
return this.api.get<Recipe>(`/recipe/${id}`)
}
save(recipe: Recipe): Observable<Recipe> {
const body = {
...recipe,
companyId: recipe.company
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
}
return this.api.post<Recipe>('/recipe', body)
}
update(recipe: Recipe, steps: Map<number, RecipeStep[]>) {
const body = {
...recipe,
steps: []
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
}
steps.forEach((groupSteps, groupId) => {
@ -84,8 +85,4 @@ 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

@ -5,9 +5,6 @@ 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'
@ -16,10 +13,7 @@ import {CreTablesModule} from '../shared/components/tables/tables.module'
imports: [
CommonModule,
CompanyRoutingModule,
SharedModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
SharedModule
]
})
export class CompanyModule { }

View File

@ -1,26 +1,7 @@
<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>
<cre-entity-list
[entities$]="companies$"
[columns]="columns"
[buttons]="buttons"
addLink="/catalog/company/add"
addPermission="EDIT_COMPANIES">
</cre-entity-list>

View File

@ -5,8 +5,6 @@ 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',
@ -14,14 +12,18 @@ import {AccountService} from '../../../accounts/services/account.service'
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
companies$ = this.companyService.all.pipe(tap(companies => this.companiesEmpty = companies.length <= 0))
companiesEmpty = false
columns = ['name', 'editButton']
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
}]
constructor(
private companyService: CompanyService,
private accountService: AccountService,
private appState: AppState,
errorService: ErrorService,
router: Router,
@ -30,8 +32,4 @@ 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

@ -16,7 +16,7 @@
tooltip="Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
'Ctrl+F5')."
[configControl]="getConfigControl(keys.INSTANCE_LOGO_SET)" previewWidth="170px"
[configControl]="getConfigControl(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
(invalidFormat)="invalidFormatConfirmBox.show()">
</cre-image-config>
@ -25,7 +25,7 @@
tooltip="Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
'Ctrl+F5')."
[configControl]="getConfigControl(keys.INSTANCE_ICON_SET)" previewWidth="32px"
[configControl]="getConfigControl(keys.INSTANCE_ICON_PATH)" previewWidth="32px"
(invalidFormat)="invalidFormatConfirmBox.show()">
</cre-image-config>
</cre-config-list>

View File

@ -16,8 +16,8 @@ export class CreConfigEditor extends ErrorHandlingComponent {
keys = {
INSTANCE_NAME: Config.INSTANCE_NAME,
INSTANCE_LOGO_SET: Config.INSTANCE_LOGO_SET,
INSTANCE_ICON_SET: Config.INSTANCE_ICON_SET,
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
INSTANCE_URL: Config.INSTANCE_URL,
DATABASE_URL: Config.DATABASE_URL,
DATABASE_USER: Config.DATABASE_USER,

View File

@ -18,7 +18,7 @@
<div class="image-wrapper d-flex flex-column justify-content-end">
<div>
<img
[src]="updatedImage ? updatedImage : imageUrl"
[src]="updatedImage ? updatedImage : configuredImageUrl"
[attr.width]="previewWidth ? previewWidth : null"
class="mat-elevation-z3"/>
</div>

View File

@ -1,20 +1,10 @@
import {
AfterViewInit,
Component,
ContentChild,
Directive,
EventEmitter,
Input,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core'
import {AfterViewInit, Component, ContentChild, Directive, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core'
import {ConfigService} from '../shared/service/config.service'
import {Config, ConfigControl} from '../shared/model/config.model'
import {SubscribingComponent} from '../shared/components/subscribing.component'
import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {formatDate, formatDateTime, getConfiguredImageUrl, getFileUrl, readFile} from '../shared/utils/utils'
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
import {AbstractControl} from '@angular/forms'
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@ -75,6 +65,10 @@ abstract class _CreConfigBase extends SubscribingComponent {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
}
get config(): Config {
return this.configControl.config
}
@ -131,9 +125,9 @@ export class CreImageConfig extends _CreConfigBase {
readFile(file, (content) => this.updatedImage = content)
}
get imageUrl(): string {
const path = this.config.key == Config.INSTANCE_ICON_SET ? 'icon' : 'logo'
return getConfiguredImageUrl(path)
get configuredImageUrl(): string {
return getFileUrl(this.config.content)
}
}

View File

@ -6,9 +6,6 @@ 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({
@ -16,10 +13,7 @@ import {CreTablesModule} from '../shared/components/tables/tables.module'
imports: [
CommonModule,
MaterialTypeRoutingModule,
SharedModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
SharedModule
]
})
export class MaterialTypeModule { }

View File

@ -1,40 +1,7 @@
<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>
<cre-entity-list
[entities$]="materialTypes$"
[columns]="columns"
[buttons]="buttons"
addLink="/catalog/materialtype/add"
addPermission="EDIT_MATERIAL_TYPES">
</cre-entity-list>

View File

@ -5,8 +5,6 @@ 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',
@ -14,16 +12,23 @@ import {AccountService} from '../../../accounts/services/account.service'
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
materialTypes$ = this.materialTypeService.all.pipe(
tap(materialTypes => this.materialTypesEmpty = materialTypes.length <= 0)
)
materialTypesEmpty = false
columns = ['name', 'prefix', 'usePercentages', 'editButton']
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
}
]
constructor(
private materialTypeService: MaterialTypeService,
private accountService: AccountService,
private appState: AppState,
errorService: ErrorService,
router: Router,
@ -32,8 +37,4 @@ 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,13 +6,9 @@ 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 {RecipesModule} from '../recipes/recipes.module'
import {ColorsModule} from '../colors/colors.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({
@ -21,13 +17,9 @@ import {CreActionBarModule} from '../shared/components/action-bar/action-bar.mod
CommonModule,
MaterialRoutingModule,
SharedModule,
RecipesModule,
ColorsModule,
MatSortModule,
FormsModule,
CreTablesModule,
CreInputsModule,
CreButtonsModule,
CreActionBarModule
FormsModule
]
})
export class MaterialModule {

View File

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

View File

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

View File

@ -1,59 +1,62 @@
<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>
<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-unit-selector [(unit)]="units"></cre-unit-selector>
<cre-accent-button
<button
*ngIf="canEditMaterial"
class="ml-3"
mat-raised-button
color="accent"
routerLink="/catalog/material/add">
Ajouter
</cre-accent-button>
</cre-action-group>
</cre-action-bar>
</button>
</div>
</div>
<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"
<table
mat-table
matSort
class="mx-auto"
[filterPredicate]="materialFilterPredicate"
[filter]="filter"
[data]="materials"
[columns]="columns">
[dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Code</th>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Code</th>
<td mat-cell *matCellDef="let material">{{material.name}}</td>
</ng-container>
<ng-container matColumnDef="materialType">
<th mat-header-cell *matHeaderCellDef>Type de produit</th>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type de produit</th>
<td mat-cell *matCellDef="let material">{{material.materialType.name}}</td>
</ng-container>
@ -65,7 +68,9 @@
<ng-container matColumnDef="addQuantity">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canAddToInventory" *matCellDef="let material; let i = index">
<div [creInteractiveCell]="i" class="input-group">
<div
[hidden]="!((!hoveredMaterial && i === 0) || (hoveredMaterial === material) || (selectedMaterial && selectedMaterial === material))"
class="input-group">
<input
#addQuantityInput
class="form-control w-50"
@ -86,7 +91,7 @@
</ng-container>
<ng-container matColumnDef="lowQuantityIcon">
<th mat-header-cell *matHeaderCellDef></th>
<th mat-header-cell *matHeaderCellDef mat-sort-header></th>
<td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)">
<mat-icon
svgIcon="format-color-fill"
@ -98,7 +103,7 @@
<ng-container matColumnDef="simdutIcon">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material" [class.disabled]="material.hasSimdut">
<td mat-cell *matCellDef="let material" [class.disabled]="materialHasSimdut(material)">
<mat-icon
svgIcon="text-box-remove"
color="warn"
@ -110,23 +115,37 @@
<ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial">
<cre-accent-button
[creInteractiveCell]="i"
routerLink="/catalog/material/edit/{{material.id}}">
Modifier
</cre-accent-button>
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
<button
mat-raised-button
color="accent"
routerLink="/catalog/material/edit/{{material.id}}">
Modifier
</button>
</ng-container>
</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">
<cre-accent-button
[creInteractiveCell]="i"
[disabled]="!material.hasSimdut"
(click)="openSimdut(material)">
Fiche signalitique
</cre-accent-button>
<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>
</td>
</ng-container>
</cre-table>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr
mat-row
class="entity-row"
*matRowDef="let material; columns: columns"
(mouseover)="hoveredMaterial = material">
</tr>
</table>

View File

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

View File

@ -4,17 +4,14 @@ 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, materialFilterFieldSeparator, materialMatchesFilter, openSimdut} from '../../../shared/model/material.model'
import {Material, 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',
@ -24,10 +21,9 @@ import {round} from '../../../shared/utils/utils'
export class InventoryComponent extends ErrorHandlingComponent {
@ViewChild(MatSort) sort: MatSort
materials: Material[] | null = []
materialTypesEntries$ = this.materialTypeService.all.pipe(map(materialTypes => {
return materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))
}))
materials: Material[] | null
materialTypes$ = this.materialTypeService.all
dataSource: MatTableDataSource<Material>
columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton']
hoveredMaterial: Material | null
@ -35,16 +31,8 @@ export class InventoryComponent extends ErrorHandlingComponent {
units = UNIT_MILLILITER
lowQuantityThreshold = 100 // TEMPORARY will be in the application settings
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)
materialTypeFilter = 1
materialNameFilter = ''
constructor(
private materialService: MaterialService,
@ -64,26 +52,39 @@ export class InventoryComponent extends ErrorHandlingComponent {
super.ngOnInit()
this.subscribe(
this.materialService.all,
materials => this.materials = materials,
this.materialService.allNotMixType,
materials => {
this.materials = materials
this.dataSource = this.setupDataSource()
},
true,
1
)
}
this.subscribe(
this.materialTypeFilterControl.valueChanges,
filter => this.materialTypeFilter = filter
)
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.materialNameFilterControl.valueChanges,
filter => this.materialNameFilter = filter
)
this.dataSource.sort = this.sort
return this.dataSource
}
this.subscribe(
this.hideLowQuantityControl.valueChanges,
filter => this.hideLowQuantity = filter
)
filterDataSource() {
this.dataSource.filter = 'filter'
}
isLowQuantity(material: Material) {
@ -91,7 +92,11 @@ export class InventoryComponent extends ErrorHandlingComponent {
}
getQuantity(material: Material): number {
return round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units), 2)
return Math.round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units) * 100) / 100
}
materialHasSimdut(material: Material): boolean {
return material.simdutUrl != null
}
openSimdut(material: Material) {
@ -113,10 +118,6 @@ 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,6 +17,10 @@ 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}`)
}
@ -29,6 +33,14 @@ 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

@ -1,10 +0,0 @@
<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,36 +0,0 @@
<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

@ -1,52 +0,0 @@
<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,21 +0,0 @@
<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,110 +0,0 @@
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

@ -1,20 +0,0 @@
<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

@ -1,28 +0,0 @@
<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

@ -1,13 +0,0 @@
<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

@ -1,13 +0,0 @@
<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

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

View File

@ -1,76 +0,0 @@
<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

@ -1,278 +0,0 @@
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
}

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