Compare commits

...

40 Commits

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

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,10 @@ import {EditComponent} from './pages/edit/edit.component';
import {RecipesModule} from '../recipes/recipes.module'
import {MatSortModule} from '@angular/material/sort'
import {FormsModule} from '@angular/forms'
import {CreTablesModule} from '../shared/components/tables/tables.module'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
@NgModule({
@ -19,7 +23,11 @@ import {FormsModule} from '@angular/forms'
SharedModule,
RecipesModule,
MatSortModule,
FormsModule
FormsModule,
CreTablesModule,
CreInputsModule,
CreButtonsModule,
CreActionBarModule
]
})
export class MaterialModule {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-submit-button [form]="recipeForm.creForm" (submit)="recipeForm.submit()"></cre-submit-button>
<cre-form-submit-button [form]="recipeForm.creForm" (submit)="recipeForm.submit()"></cre-form-submit-button>
</cre-action-group>
</cre-action-bar>

View File

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

View File

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

View File

@ -1,126 +0,0 @@
<ng-container *ngIf="recipe && materials && (!editionMode || mix)">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/edit/{{recipeId}}">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-warn-button *ngIf="editionMode" (click)="deleteConfirmBox.show()">Supprimer</cre-warn-button>
<cre-submit-button [form]="creForm" (submit)="submit()">Enregistrer</cre-submit-button>
</cre-action-group>
</cre-action-bar>
<cre-form #creForm [formControls]="controls" class="mx-auto">
<cre-form-title *ngIf="!editionMode">Ajouter un mélange à la couleur {{recipe.company.name}}
- {{recipe.name}}</cre-form-title>
<cre-form-title *ngIf="editionMode">Modification du mélange {{mix.mixType.name}} de la
couleur {{recipe.company.name}}
- {{recipe.name}}</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>
</ng-container>
<table #matTable mat-table [dataSource]="mixMaterials" class="mx-auto mt-5">
<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>-->
<cre-select [entries]="getAvailableMaterials(mixMaterial)"></cre-select>
</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="tableColumns"></tr>
<tr mat-row *matRowDef="let mixMaterial; columns: tableColumns" (mouseover)="hoveredMixMaterial = mixMaterial"></tr>
</table>
<cre-confirm-box
*ngIf="editionMode && mix"
#deleteConfirmBox
message="Voulez-vous vraiment supprimer le mélange {{mix.mixType.name}} de la recette {{recipe.company.name}} - {{recipe.name}}"
(confirm)="delete()">
</cre-confirm-box>

View File

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

View File

@ -1,233 +0,0 @@
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
import {
Mix,
MixMaterial,
MixMaterialDto,
mixMaterialsAsMixMaterialsDto,
Recipe,
sortMixMaterialsDto
} from '../../../shared/model/recipe.model'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {MixService} from '../../services/mix.service'
import {RecipeService} from '../../services/recipe.service'
import {Material, materialComparator} from '../../../shared/model/material.model'
import {MaterialService} from '../../../material/service/material.service'
import {MaterialTypeService} from '../../../material-type/service/material-type.service'
import {FormBuilder, FormControl, 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'
import {map, tap} from 'rxjs/operators';
import {CreInputEntry} from '../../../shared/components/inputs/inputs';
import {CreForm, ICreForm} from '../../../shared/components/forms/forms';
@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
@ViewChild(CreForm) creForm: ICreForm
@Input() mixId: number | null
@Input() recipeId: number | null
@Input() materials: Material[]
@Output() save = new EventEmitter<any>()
recipe: Recipe
mix: Mix | null
editionMode = false
units = UNIT_MILLILITER
hoveredMixMaterial: MixMaterial | null
tableColumns = ['position', 'buttonsPosition', 'material', 'quantity', 'units', 'buttonRemove']
deleting = false
errorHandlers = [{
filter: error => error.type === 'notfound-mix-id',
consumer: _ => 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: _ => 'Ce mélange est utilisé par un ou plusieurs autres mélanges'
}, {
filter: error => error.type === 'invalid-mixmaterial-first',
messageProducer: _ => 'La quantité du premier ingrédient du mélange ne peut pas être exprimée en pourcentage'
}]
controls: any
materialTypeEntries$ = this.materialTypeService.all.pipe(
map(materialTypes => materialTypes.map(materialType => new CreInputEntry(materialType.id, materialType.name))),
)
private _mixMaterials: MixMaterialDto[] = []
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.fetchRecipe()
this.fetchMaterials()
}
private fetchRecipe() {
this.subscribe(
this.recipeService.getById(this.recipeId),
recipe => {
this.recipe = recipe
if (this.editionMode) {
this.mix = this.recipe.mixes.find(mix => mix.id === this.mixId)
this.mixMaterials = mixMaterialsAsMixMaterialsDto(this.mix)
} else {
this.addBlankMixMaterial()
}
this.createControls()
}
)
}
private fetchMaterials() {
this.subscribe(
this.materialService.all,
materials => this.materials = materials
)
}
private createControls() {
this.controls = {
name: new FormControl(this.mix?.mixType.name, Validators.required),
materialType: new FormControl(this.mix?.mixType.material.materialType.id, Validators.required)
}
}
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.controls.name.value,
recipeId: this.recipeId,
materialTypeId: this.controls.materialType.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): CreInputEntry[] {
// return this.materialService.all.pipe(
// map(materials => materials.filter(material => {
// return mixMaterial.materialId === material.id || this.mixMaterials.filter(mm => mm.materialId === material.id).length === 0
// })),
// map(materials => this.sortedMaterials(materials)),
// map(materials => materials.map(material => new CreInputEntry(material.id, material.name)))
// )
console.log(this.materials.map(material => new CreInputEntry(material.id, material.name)))
return []
// return this.materials.map(material => new CreInputEntry(material.id, material.name))
// return this.materials
// .filter(m => mixMaterial.materialId === m.id || this.mixMaterials.filter(mm => mm.materialId === m.id).length === 0)
// .sort(materialComparator)
// .map(material => new CreInputEntry(material.id, material.name))
}
materialDisplayName(material: Material): string {
if (material.materialType.prefix) {
return `[${material.materialType.prefix}] ${material.name}`
}
return material.name
}
get mixMaterials(): MixMaterialDto[] {
return this._mixMaterials
}
set mixMaterials(mixMaterials: MixMaterialDto[]) {
this._mixMaterials = mixMaterials
this.mixTable.renderRows()
}
private addBlankMixMaterial() {
const mixMaterial = new MixMaterialDto(null, 0, false, this.mixMaterials.length + 1)
this.mixMaterials = [...this.mixMaterials, mixMaterial]
}
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]="!hasSimdut(getMixMaterialFromDto(mixMaterial).material)"
[disabled]="!getMixMaterialFromDto(mixMaterial).material.hasSimdut"
(click)="openSimdut(getMixMaterialFromDto(mixMaterial))">
Fiche signalitique
</button>

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@
</div>
<!-- Images -->
<div *ngIf="recipe.imagesUrls">
<div>
<cre-images-editor [recipe]="recipe" [editionMode]="false"></cre-images-editor>
</div>
</div>

View File

@ -143,7 +143,7 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
}
deductMix() {
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === this.deductedMixId)[0].mixMaterials[0]
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === this.deductedMixId)[0].mixQuantities[0]
if (this.quantitiesChanges.has(this.deductedMixId) && this.quantitiesChanges.get(this.deductedMixId).has(firstMixMaterial.material.id)) {
const originalQuantity = firstMixMaterial.quantity
const currentQuantity = this.quantitiesChanges.get(this.deductedMixId).get(firstMixMaterial.material.id)

View File

@ -36,7 +36,12 @@
<ng-template
#recipeTableTemplate
let-recipes="recipes">
<table class="mx-auto" mat-table [dataSource]="recipes">
<cre-table
class="w-100"
[columns]="columns"
[data]="recipes"
[filter]="searchQuery"
[filterPredicate]="recipeFilterPredicate">
<!-- Recipe's info -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
@ -84,19 +89,16 @@
<!-- Buttons -->
<ng-container matColumnDef="buttonView">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe">
<button mat-flat-button color="accent" routerLink="/color/explore/{{recipe.id}}">Voir</button>
<td mat-cell *matCellDef="let recipe; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/explore/{{recipe.id}}">Voir</cre-accent-button>
</td>
</ng-container>
<ng-container matColumnDef="buttonEdit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let recipe" [class.disabled]="!hasEditPermission">
<button mat-flat-button color="accent" routerLink="/color/edit/{{recipe.id}}">Modifier</button>
<td mat-cell *matCellDef="let recipe; let i = index" [class.disabled]="!hasEditPermission">
<cre-accent-button [creInteractiveCell]="i" routerLink="/color/edit/{{recipe.id}}">Modifier</cre-accent-button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let recipe; columns: tableCols" [hidden]="hiddenRecipes[recipe.id]"></tr>
</table>
</cre-table>
</ng-template>

View File

@ -1,17 +1,17 @@
import {ChangeDetectorRef, Component} from '@angular/core';
import {ErrorHandlingComponent} from '../shared/components/subscribing.component';
import {Company} from '../shared/model/company.model';
import {getRecipeLuma, Recipe} 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';
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',
@ -21,12 +21,13 @@ import {FormControl} from '@angular/forms';
export class RecipeList extends ErrorHandlingComponent {
companies: Company[] = []
recipes: Map<number, Recipe[]> = new Map<number, Recipe[]>()
tableCols = ['name', 'description', 'color', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
searchQuery = ''
columns = ['name', 'description', 'color', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
panelForcedExpanded = false
hiddenRecipes = []
searchControl: FormControl
searchQuery = ''
recipeFilterPredicate = recipeMatchesFilter
constructor(
private companyService: CompanyService,
@ -62,7 +63,13 @@ export class RecipeList extends ErrorHandlingComponent {
this.searchControl = new FormControl('')
this.subscribe(
this.searchControl.valueChanges,
value => this.searchRecipes(value)
value => {
this.searchQuery = value
if (value.length > 0 && !this.panelForcedExpanded) {
this.panelForcedExpanded = true
this.cdRef.detectChanges()
}
}
)
}
@ -81,25 +88,14 @@ export class RecipeList extends ErrorHandlingComponent {
)
}
searchRecipes(searchQuery) {
this.searchQuery = searchQuery
if (this.searchQuery.length > 0 && !this.panelForcedExpanded) {
this.panelForcedExpanded = true
this.cdRef.detectChanges()
}
for (let recipeArray of this.recipes.values()) {
recipeArray.forEach(recipe => this.recipeMatchesSearchQuery(recipe))
}
}
isCompanyHidden(company: Company): boolean {
const companyRecipes = this.recipes.get(company.id);
const companyRecipes = this.recipes.get(company.id)
return !(companyRecipes && companyRecipes.length >= 0) ||
this.searchQuery && this.searchQuery.length > 0 &&
companyRecipes.map(r => this.hiddenRecipes[r.id]).filter(r => !r).length <= 0
!companyRecipes.some(recipe => this.recipeFilterPredicate(recipe, this.searchQuery))
}
isLight(recipe: Recipe): boolean {
return getRecipeLuma(recipe) > 200
}
@ -111,16 +107,4 @@ export class RecipeList extends ErrorHandlingComponent {
get hasCompanyEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
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,20 @@
<ng-container *ngIf="recipe">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/edit/{{recipe.id}}">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-accent-button [disabled]="!form.valid" (click)="submit(form.formValues)">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<cre-mix-form
#form
[recipe]="recipe"
[materialTypes]="materialTypes$"
[materials]="materials$">
<cre-form-title>
Ajouter un mélange à la couleur {{recipe.company.name}} - {{recipe.name}}
</cre-form-title>
</cre-mix-form>
</ng-container>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {CreRecipeExplore} from './explore';
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component';
import {MixAddComponent} from './pages/mix/mix-add/mix-add.component';
import {RecipeAdd, RecipeEdit} from './recipes';
import {RecipeList} from './list';
import {NgModule} from '@angular/core'
import {RouterModule, Routes} from '@angular/router'
import {CreRecipeExplore} from './explore'
import {RecipeAdd, RecipeEdit} from './recipes'
import {RecipeList} from './list'
import {MixAdd, MixEdit} from './mix/mix'
const routes: Routes = [{
path: 'list',
@ -17,10 +16,10 @@ const routes: Routes = [{
component: RecipeEdit
}, {
path: 'add/mix/:recipeId',
component: MixAddComponent
component: MixAdd
}, {
path: 'edit/mix/:recipeId/:id',
component: MixEditComponent
component: MixEdit
}, {
path: 'explore/:id',
component: CreRecipeExplore

View File

@ -9,18 +9,18 @@ import {RecipeInfoComponent} from './components/recipe-info/recipe-info.componen
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'
import {CreInputsModule} from '../shared/components/inputs/inputs.module';
import {CreButtonsModule} from '../shared/components/buttons/buttons.module';
import {RecipeAdd, RecipeEdit, RecipeForm} from './recipes';
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module';
import {RecipeList} from './list';
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {RecipeAdd, RecipeEdit, RecipeForm} from './recipes'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {RecipeList} from './list'
import {MixAdd, MixEdit, MixForm, MixInfoForm} from './mix/mix'
import {CreTablesModule} from '../shared/components/tables/tables.module'
import {MixMaterialsForm, MixMaterialsFormComboBox} from './mix/materials-form'
@NgModule({
declarations: [
@ -29,16 +29,19 @@ import {RecipeList} from './list';
MixTableComponent,
StepListComponent,
StepTableComponent,
MixEditorComponent,
UnitSelectorComponent,
MixAddComponent,
MixEditComponent,
ImagesEditorComponent,
MixesCardComponent,
RecipeForm,
RecipeAdd,
RecipeEdit,
RecipeList
RecipeList,
MixAdd,
MixEdit,
MixForm,
MixInfoForm,
MixMaterialsForm,
MixMaterialsFormComboBox
],
exports: [
UnitSelectorComponent
@ -51,7 +54,8 @@ import {RecipeList} from './list';
MatSortModule,
CreInputsModule,
CreButtonsModule,
CreActionBarModule
CreActionBarModule,
CreTablesModule
]
})
export class RecipesModule {

View File

@ -16,3 +16,15 @@ mat-expansion-panel
.recipe-content > div
margin: 0 3rem 3rem
cre-table
.mat-column-name,
.mat-column-color,
.mat-column-iconNotApproved
width: 5em
.mat-column-description
width: 50em
.mat-column-sample
width: 10em

View File

@ -25,10 +25,10 @@ import {CreForm, ICreForm} from '../shared/components/forms/forms';
encapsulation: ViewEncapsulation.None
})
export class RecipeForm extends SubscribingComponent {
@Input() recipe: Recipe | null
@ViewChild(CreForm) creForm: ICreForm
@Input() recipe: Recipe | null
@Output() submitForm = new EventEmitter<Recipe>();
controls: any
@ -46,7 +46,7 @@ export class RecipeForm extends SubscribingComponent {
}
ngOnInit() {
super.ngOnInit();
super.ngOnInit()
this.fetchCompanies()

View File

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

View File

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

View File

@ -84,4 +84,8 @@ export class RecipeService {
delete(id: number): Observable<void> {
return this.api.delete<void>(`/recipe/${id}`)
}
getImagesIds(id: number): Observable<string[]> {
return this.api.get<string[]>(`/recipe/${id}/image`)
}
}

View File

@ -8,7 +8,7 @@ import {Title} from '@angular/platform-browser'
})
export class AppState {
private readonly KEY_AUTHENTICATED = 'authenticated'
private readonly KEY_AUTHENTICATION_EXPIRATION = 'authentication-expiration'
private readonly KEY_DEFAULT_GROUP_USER_AUTHENTICATED = 'default-group-user-authenticated'
private readonly KEY_LOGGED_IN_USER = 'logged-in-user'
authenticatedUser$ = new Subject<{ authenticated: boolean, authenticatedUser: User }>()
@ -19,9 +19,19 @@ export class AppState {
) {
}
authenticateUser(user: User) {
this.authenticatedUser = user
this.isAuthenticated = true
}
authenticateGroupUser(user: User) {
this.authenticatedUser = user
this.isDefaultGroupUserAuthenticated = true
}
resetAuthenticatedUser() {
this.isAuthenticated = false
this.authenticationExpiration = -1
this.isDefaultGroupUserAuthenticated = false
this.authenticatedUser = null
}
@ -36,7 +46,7 @@ export class AppState {
return sessionStorage.getItem(this.KEY_AUTHENTICATED) === 'true'
}
set isAuthenticated(value: boolean) {
private set isAuthenticated(value: boolean) {
sessionStorage.setItem(this.KEY_AUTHENTICATED, value.toString())
this.authenticatedUser$.next({
authenticated: value,
@ -44,12 +54,16 @@ export class AppState {
})
}
get authenticationExpiration(): number {
return parseInt(sessionStorage.getItem(this.KEY_AUTHENTICATION_EXPIRATION))
get isDefaultGroupUserAuthenticated(): boolean {
return sessionStorage.getItem(this.KEY_DEFAULT_GROUP_USER_AUTHENTICATED) === 'true'
}
set authenticationExpiration(value: number) {
sessionStorage.setItem(this.KEY_AUTHENTICATION_EXPIRATION, value.toString())
private set isDefaultGroupUserAuthenticated(value: boolean) {
sessionStorage.setItem(this.KEY_DEFAULT_GROUP_USER_AUTHENTICATED, value.toString())
}
get hasCredentials(): boolean {
return this.isAuthenticated || this.isDefaultGroupUserAuthenticated
}
get authenticatedUser(): User {
@ -57,9 +71,9 @@ export class AppState {
return userString ? JSON.parse(userString) : null
}
set authenticatedUser(value: User) {
private set authenticatedUser(value: User) {
if (value === null) {
sessionStorage.removeItem(this.KEY_LOGGED_IN_USER)
// sessionStorage.removeItem(this.KEY_LOGGED_IN_USER)
} else {
sessionStorage.setItem(this.KEY_LOGGED_IN_USER, JSON.stringify(value))
}

View File

@ -1,13 +1,14 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Component, ContentChild, EventEmitter, Input, Output} from '@angular/core'
import {ICreForm} from './forms';
@Component({
selector: 'cre-submit-button',
selector: 'cre-form-submit-button',
templateUrl: 'submit-button.html'
})
export class CreSubmitButton {
@Input() form: ICreForm
@Input() valid: boolean | null
@Input() text = 'Enregistrer'
@Output() submit = new EventEmitter<void>()

View File

@ -5,7 +5,7 @@
</mat-card-title>
</mat-card-header>
<mat-card-content [class.no-action]="!hasActions">
<form [formGroup]="form">
<form [formGroup]="formGroup">
<ng-content select="cre-form-content"></ng-content>
</form>
</mat-card-content>

View File

@ -2,7 +2,7 @@ import {Component, ContentChild, Directive, Input, OnInit, ViewEncapsulation} fr
import {FormBuilder, FormGroup} from '@angular/forms'
export interface ICreForm {
form: FormGroup
formGroup: FormGroup
valid: boolean
invalid: boolean
}
@ -35,7 +35,7 @@ export class CreForm implements ICreForm, OnInit {
@ContentChild(CreFormActions) formActions: CreFormActions
@Input() formControls: { [key: string]: any }
form: FormGroup
formGroup: FormGroup
constructor(
private formBuilder: FormBuilder
@ -43,7 +43,7 @@ export class CreForm implements ICreForm, OnInit {
}
ngOnInit(): void {
this.form = this.formBuilder.group(this.formControls)
this.formGroup = this.formBuilder.group(this.formControls)
}
get hasActions(): boolean {
@ -51,10 +51,10 @@ export class CreForm implements ICreForm, OnInit {
}
get valid(): boolean {
return this.form && this.form.valid
return this.formGroup && this.formGroup.valid
}
get invalid(): boolean {
return !this.form || this.form.invalid
return !this.formGroup || this.formGroup.invalid
}
}

View File

@ -1 +1 @@
<cre-accent-button [disabled]="disableButton" (click)="submit.emit()">Enregistrer</cre-accent-button>
<cre-accent-button [disabled]="disableButton" (click)="submit.emit()">{{text}}</cre-accent-button>

View File

@ -58,9 +58,10 @@ export class HeaderComponent extends SubscribingComponent {
}
ngOnDestroy(): void {
this.accountService.logout(() => {
console.log('Successfully logged out')
})
this.subscribe(
this.accountService.logout(),
() => console.info('Successfully logged out')
)
super.ngOnDestroy()
}
@ -71,7 +72,7 @@ export class HeaderComponent extends SubscribingComponent {
set activeLink(link: string) {
this._activeLink = link
this.router.navigate([link])
this.urlUtils.navigateTo(link)
}
get activeLink() {

View File

@ -1,4 +1,4 @@
<mat-form-field>
<mat-form-field *ngIf="internalControl">
<mat-label>{{label}}</mat-label>
<input
matInput
@ -16,8 +16,12 @@
</mat-error>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let entry of entries | async" [value]="entry.value">
{{entry.display || entry.value}}
<mat-option
*ngFor="let entry of filteredEntries"
[value]="entry.value"
[class.font-weight-bold]="entry.styleOptions?.bold"
[title]="entry.value">
{{entry.display ? entry.display : entry.value}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

View File

@ -1,5 +1,6 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChild,
Directive,
@ -50,7 +51,13 @@ export class CreInputComponent extends _CreInputBase implements AfterViewInit {
@ViewChild('input') input: any
@ContentChild(TemplateRef) errors: TemplateRef<any>
fieldRequired = true
fieldRequired = false
constructor(
private cdRef: ChangeDetectorRef
) {
super()
}
ngAfterViewInit() {
const element = this.input.nativeElement
@ -60,6 +67,8 @@ export class CreInputComponent extends _CreInputBase implements AfterViewInit {
element.autocomplete = this.autocomplete ? 'on' : 'off'
this.fieldRequired = this.control ? this.control.validator && this.control.validator({} as AbstractControl)?.required : this.required
this.cdRef.detectChanges()
}
}
@ -148,42 +157,51 @@ export class CreChipInputComponent implements OnInit {
templateUrl: 'combo-box.html',
encapsulation: ViewEncapsulation.None
})
export class CreComboBoxComponent implements OnInit {
export class CreComboBoxComponent {
@Input() control: AbstractControl
@Input() label: string
@Input() icon: string
@Input() required = true
@Input() entries: Observable<CreInputEntry[]>
@ContentChild(TemplateRef) errors: TemplateRef<any>
internalControl: FormControl
validValue = false
filteredEntries: CreInputEntry[]
private _destroy$ = new Subject<boolean>();
private _destroy$ = new Subject<boolean>()
private _entries: CreInputEntry[]
private _controlsInitialized = false
ngOnInit() {
this.entries.pipe(takeUntil(this._destroy$))
.subscribe({
next: entries => {
this._entries = entries
if (this.control.value) {
this.internalControl.setValue(this.findEntryByKey(this.control.value)?.value)
@Input()
set entries(entries: Observable<CreInputEntry[]> | CreInputEntry[]) {
if (isObservable(entries)) {
(entries as Observable<CreInputEntry[]>).pipe(takeUntil(this._destroy$))
.subscribe({
next: entries => {
this.initControls(entries)
}
})
} else {
this.initControls((entries as CreInputEntry[]))
}
}
if (this.control.disabled) {
this.internalControl.disable()
}
}
})
reloadEntries() {
this.filteredEntries = this.filterEntries(this.internalControl.value)
}
private initControls(entries) {
this._entries = entries
if (this._controlsInitialized) {
return
}
this.internalControl = new FormControl({
value: null,
disabled: false
}, Validators.compose([this.control.validator, this.valueValidator()]))
this.internalControl.valueChanges.pipe(takeUntil(this._destroy$))
this.internalControl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe({
next: value => {
if (this.internalControl.valid) {
@ -191,8 +209,36 @@ export class CreComboBoxComponent implements OnInit {
} else {
this.control.setValue(null)
}
this.filteredEntries = this.filterEntries(value)
}
})
if (this.control.value) {
this.internalControl.setValue(this.findEntryByKey(this.control.value)?.value)
}
if (this.control.disabled) {
this.internalControl.disable()
}
this.reloadEntries()
this._controlsInitialized = true
}
private filterEntries(value: string): CreInputEntry[] {
if (!value) {
return this._entries
}
const valueLowerCase = value.toLowerCase()
return this._entries.filter(entry => {
if (entry.display) {
return entry.display.toLowerCase().includes(valueLowerCase)
} else {
return entry.value.toLowerCase().includes(valueLowerCase)
}
})
}
private findEntryByKey(key: any): CreInputEntry | null {
@ -400,7 +446,7 @@ export class CreSliderInputComponent {
templateUrl: 'select.html'
})
export class CreSelectComponent extends _CreInputBase {
@Input() entries: Observable<CreInputEntry[]> | CreInputEntry[]
@Input() entries: CreInputEntry[] | Observable<CreInputEntry[]>
get entriesAreObservable(): boolean {
return isObservable(this.entries)
@ -419,11 +465,16 @@ export class CreInputEntry {
constructor(
public key: any,
public value: any,
public display?: any
public display?: any,
public styleOptions?: CreInputEntryStyleOptions
) {
}
}
export interface CreInputEntryStyleOptions {
bold?: boolean
}
export function chipListRequired(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return !control.value || control.value.length <= 0 ? {required: true} : null

View File

@ -1,14 +1,13 @@
<mat-form-field>
<mat-label>{{label}}</mat-label>
<mat-select [attr.formControl]="control ? control : null">
<mat-select [formControl]="control">
<ng-container *ngIf="entriesAreObservable">
<ng-container
*ngFor="let entry of (observableEntries | async)">
<mat-option [value]="entry.key">{{entry.display || entry.value}}</mat-option>
</ng-container>
</ng-container>
<ng-container
*ngIf="!entriesAreObservable">
<ng-container *ngIf="!entriesAreObservable">
<mat-option *ngFor="let entry of arrayEntries" [value]="entry.key">
{{entry.display || entry.value}}
</mat-option>

View File

@ -0,0 +1,17 @@
<ng-container *ngIf="!hidden">
<button
mat-mini-fab
color="primary"
class="mr-1"
[disabled]="disableDecreaseButton || position <= min"
(click)="decreasePosition()">
<mat-icon svgIcon="arrow-up"></mat-icon>
</button>
<button
mat-mini-fab
color="primary"
[disabled]="disableIncreaseButton || position >= max"
(click)="increasePosition()">
<mat-icon svgIcon="arrow-down"></mat-icon>
</button>
</ng-container>

View File

@ -1,4 +1,6 @@
<table mat-table [dataSource]="dataSource">
<table
mat-table
[dataSource]="dataSource">
<ng-content></ng-content>
<tr mat-header-row *matHeaderRowDef="columns"></tr>

View File

@ -1,20 +1,28 @@
import {NgModule} from '@angular/core'
import {MatTableModule} from '@angular/material/table'
import {CommonModule} from '@angular/common'
import {CreInteractiveCell, CreTable} from './tables'
import {CreInteractiveCell, CrePositionButtons, CreTable} from './tables'
import {MatButtonModule} from "@angular/material/button";
import {MatIconModule} from "@angular/material/icon";
import {MatSortModule} from '@angular/material/sort'
@NgModule({
declarations: [
CreTable,
CreInteractiveCell
],
imports: [
MatTableModule,
CommonModule
CreInteractiveCell,
CrePositionButtons
],
imports: [
MatTableModule,
CommonModule,
MatButtonModule,
MatIconModule,
MatSortModule
],
exports: [
CreTable,
CreInteractiveCell,
CrePositionButtons
]
})
export class CreTablesModule {

View File

@ -3,13 +3,15 @@ import {
Component,
ContentChildren,
Directive,
EventEmitter,
HostBinding,
Input,
Output,
QueryList,
ViewChild,
ViewEncapsulation
} from '@angular/core'
import {MatColumnDef, MatHeaderRowDef, MatRowDef, MatTable} from '@angular/material/table'
import {MatColumnDef, MatHeaderRowDef, MatRowDef, MatTable, MatTableDataSource} from '@angular/material/table'
@Directive({
selector: '[creInteractiveCell]'
@ -57,17 +59,38 @@ export class CreTable<T> implements AfterContentInit {
@ViewChild(MatTable, {static: true}) table: MatTable<T>
@Input() columns: string[]
@Input() dataSource: T[]
@Input() interactive = true
@Input() filterPredicate: (t: T, filter: string) => boolean = () => true
@Input() sortingDataAccessor: (t: T, header: string) => string | number
@Input() set filter(filter: string) {
if (this.dataSource) {
this.dataSource.filter = filter
}
}
@Input() set data(data: T[]) {
this.setupDataSource(data)
}
selectedIndex = 0
dataSource: MatTableDataSource<T>
ngAfterContentInit(): void {
this.columnDefs.forEach(columnDef => this.table.addColumnDef(columnDef))
this.rowDefs.forEach(rowDef => this.table.addRowDef(rowDef))
this.headerRowDefs.forEach(headerRowDef => this.table.addHeaderRowDef(headerRowDef))
}
private setupDataSource(data: T[]) {
this.dataSource = new MatTableDataSource<T>(data)
if (this.filterPredicate) {
this.dataSource.filterPredicate = (t, filter) => this.filterPredicate(t, filter)
}
}
onRowHover(index: number) {
if (this.interactive) {
this.interactiveCells.forEach(cell => cell.hoverIndex = index)
@ -80,4 +103,33 @@ export class CreTable<T> implements AfterContentInit {
this.interactiveCells.forEach(cell => cell.selectedIndex = index)
}
}
renderRows() {
this.table.renderRows()
}
}
@Component({
selector: 'cre-table-position-buttons',
templateUrl: 'position-buttons.html'
})
export class CrePositionButtons {
@Input() position = 0
@Input() min = 0
@Input() max: number
@Input() hidden = false
@Input() disableDecreaseButton = false
@Input() disableIncreaseButton = false
@Output() positionChange = new EventEmitter<number>()
increasePosition() {
this.position += 1
this.positionChange.emit(this.position)
}
decreasePosition() {
this.position -= 1
this.positionChange.emit(this.position)
}
}

View File

@ -1,4 +1,4 @@
import {MaterialType} from './materialtype.model';
import {MaterialType} from './materialtype.model'
import {openPdf} from '../utils/utils'
export class Material {
@ -7,13 +7,14 @@ export class Material {
public name: string,
public inventoryQuantity: number,
public materialType: MaterialType,
public simdutUrl: string
public isMixType: boolean,
public hasSimdut: boolean
) {
}
}
export function openSimdut(material: Material) {
openPdf(material.simdutUrl)
openPdf(`simdut/${material.name}`)
}
export const materialComparator = (a: Material, b: Material): number => {
@ -37,3 +38,16 @@ export const materialComparator = (a: Material, b: Material): number => {
}
}
}
// Uses private use UTF-8 char to separate the two fields, change if a better method is found
export const materialFilterFieldSeparator = '􀃿'
export function materialMatchesFilter(material: Material, filter: string): boolean {
const [materialTypeFilter, materialNameFilter, hideLowQuantity, lowQuantityThreshold] = filter.split(materialFilterFieldSeparator)
const materialTypeId = parseInt(materialTypeFilter)
const matchesMaterialType = materialTypeId === 1 || materialTypeId == material.materialType.id
const matchesMaterialName = !materialNameFilter || material.name.toLowerCase().includes(materialNameFilter.toLowerCase())
const matchesLowQuantity = material.inventoryQuantity < parseInt(lowQuantityThreshold)
return matchesMaterialType && matchesMaterialName && (hideLowQuantity === 'false' || matchesLowQuantity)
}

View File

@ -1,6 +1,8 @@
import {Material} from './material.model'
import {Company} from './company.model'
import {Group} from './user'
import {UNIT_MILLILITER} from "../units";
import {MaterialType} from "./materialtype.model";
export class Recipe {
public id: number
@ -15,7 +17,6 @@ export class Recipe {
public mixes: Mix[]
public approbationExpired: boolean
public groupsInformation: RecipeGroupInformation[]
public imagesUrls: string[]
}
export class RecipeGroupInformation {
@ -32,13 +33,13 @@ export class Mix {
constructor(
public id: number,
public mixType: MixType,
public mixMaterials: MixMaterial[],
public mixQuantities: MixQuantity[],
public location: string,
) {
}
}
export class MixMaterial {
export class MixQuantity {
constructor(
public id: number,
public material: Material,
@ -53,7 +54,9 @@ export class MixMaterialDto {
public materialId: number,
public quantity: number,
public isPercents: boolean,
public position: number
public position: number,
public units: string,
public isMixType: boolean
) {
}
}
@ -62,7 +65,7 @@ class MixType {
constructor(
public id: number,
public name: string,
public material: Material
public materialType: MaterialType
) {
}
}
@ -98,12 +101,14 @@ export function sortRecipeSteps(steps: RecipeStep[]): RecipeStep[] {
return steps.sort((a, b) => a.position - b.position)
}
export function mixMaterialsAsMixMaterialsDto(mix: Mix): MixMaterialDto[] {
return sortMixMaterialsDto(mix.mixMaterials.map(m => new MixMaterialDto(
export function mixMaterialsToMixMaterialsDto(mix: Mix): MixMaterialDto[] {
return sortMixMaterialsDto(mix.mixQuantities.map(m => new MixMaterialDto(
m.material.id,
m.quantity,
m.material.materialType.usePercentages,
m.position
m.position,
UNIT_MILLILITER,
m.material.isMixType
)))
}
@ -121,3 +126,8 @@ export function getRecipeLuma(recipe: Recipe): number {
return 0.2126 * r + 0.7152 * g + 0.0722 * b // per ITU-R BT.709
}
export function recipeMatchesFilter(recipe: Recipe, filter: string): boolean {
const recipeStr = recipe.company.name + recipe.name + recipe.description + recipe.sample
return recipeStr.toLowerCase().indexOf(filter.toLowerCase()) >= 0
}

View File

@ -4,10 +4,9 @@ import {Observable, Subject} from 'rxjs'
import {environment} from '../../../../environments/environment'
import {AppState} from '../app-state'
import {Router} from '@angular/router'
import {map, share, takeUntil, tap} from 'rxjs/operators'
import {map, share, takeUntil} from 'rxjs/operators'
import {valueOr} from '../utils/utils'
import {ErrorService} from './error.service'
import {globalLoadingWheel} from '../components/loading-wheel/loading-wheel.component'
@Injectable({
providedIn: 'root'
@ -71,14 +70,13 @@ export class ApiService implements OnDestroy {
observe: 'response'
}
if (needAuthentication) {
if (this.checkAuthenticated()) {
if (this.appState.hasCredentials) {
if (httpOptions) {
httpOptions.withCredentials = true
} else {
console.error('httpOptions need to be specified to use credentials in HTTP methods.')
}
} else {
this.appState.resetAuthenticatedUser()
this.navigateToLogin()
}
}
@ -90,11 +88,6 @@ export class ApiService implements OnDestroy {
.pipe(takeUntil(this._destroy$), map(r => r.body), share())
}
private checkAuthenticated(): boolean {
return (this.appState.isAuthenticated && Date.now() <= this.appState.authenticationExpiration) ||
(this.appState.authenticatedUser && this.appState.authenticatedUser.group != null)
}
private navigateToLogin() {
this.router.navigate(['/account/login'])
}

View File

@ -38,6 +38,7 @@ import {VarDirective} from './directives/var.directive'
import {CreColorPreview} from './components/color-preview/color-preview'
import {CreDialogsModule} from './components/dialogs/dialogs.module'
import {CreAlertsModule} from './components/alerts/alerts.module';
import {CreActionBarModule} from './components/action-bar/action-bar.module'
@NgModule({
declarations: [VarDirective, HeaderComponent, UserMenuComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent, LoadingWheelComponent, CreColorPreview],

View File

@ -25,8 +25,8 @@ export const UNIT_RATIOS = {
}
}
export function convertMixMaterialQuantity(computedQuantity: MixMaterialDto, from: string, to: string): number {
return !computedQuantity.isPercents ? convertQuantity(computedQuantity.quantity, from, to) : computedQuantity.quantity
export function convertMixMaterialQuantity(mixMaterial: MixMaterialDto, to: string): number {
return !mixMaterial.isPercents ? convertQuantity(mixMaterial.quantity, mixMaterial.units, to) : mixMaterial.quantity
}
export function convertQuantity(quantity: number, from: string, to: string): number {

View File

@ -1,5 +1,5 @@
/** Returns [value] if it is not null or [or]. */
import {DateTimeFormatter, LocalDate, LocalDateTime} from 'js-joda'
import {DateTimeFormatter, LocalDate, LocalDateTime} from '@js-joda/core'
import {TouchUpKit} from '../model/touch-up-kit.model'
import {environment} from '../../../../environments/environment'
@ -10,20 +10,28 @@ export function valueOr<T>(value: T, or: T): T {
const MEDIA_TYPE_PDF = 'application/pdf'
const MEDIA_TYPE_JPG = 'image/jpeg'
export function openPdf(url: string) {
openUrl(url, MEDIA_TYPE_PDF)
export function getImageUrl(path: string): string {
return getFileUri(`images/${path}`, MEDIA_TYPE_JPG)
}
export function openJpg(url: string) {
openUrl(url, MEDIA_TYPE_JPG)
export function getFileUri(path: string, mediaType: string): string {
return `${environment.apiUrl}/file?path=${encodeURIComponent(path)}&mediaType=${encodeURIComponent(mediaType)}`
}
export function openPdf(path: string) {
openFileUri(`pdf/${path}.pdf`, MEDIA_TYPE_PDF)
}
export function openJpg(path: string) {
openFileUri(`images/${path}`, MEDIA_TYPE_JPG)
}
export function openTouchUpKit(touchUpKit: TouchUpKit) {
openRawUrl(`${environment.apiUrl}/touchupkit/pdf?project=${touchUpKit.project}`)
}
export function openUrl(url: string, mediaType: string) {
openRawUrl(`${url}&mediaType=${encodeURIComponent(mediaType)}`)
export function openFileUri(path: string, mediaType: string) {
openRawUrl(getFileUri(path, mediaType))
}
export function openRawUrl(url: string) {
@ -64,3 +72,8 @@ export function getFileUrl(path: string) {
export function getConfiguredImageUrl(path: string) {
return `${environment.apiUrl}/config/${path}`
}
export function round(n: number, digits: number): number {
const power = Math.pow(10, digits)
return Math.round(n * power) / power
}

View File

@ -1,4 +1,4 @@
@import '../../../../custom-theme'
@import '~src/variables'
.touchupkit-finish-container
display: inline-block

View File

@ -14,7 +14,7 @@
</cre-action-group>
</cre-action-bar>
<cre-table class="mx-auto" [dataSource]="touchUpKit.content" [columns]="contentTableCols" [interactive]="false">
<cre-table class="mx-auto" [data]="touchUpKit.content" [columns]="contentTableCols" [interactive]="false">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let product">{{product.name}}</td>

View File

@ -1,11 +1,11 @@
<cre-action-bar>
<cre-action-group></cre-action-group>
<cre-action-group>
<cre-accent-button routerLink="/misc/touch-up-kit/add">Ajouter</cre-accent-button>
<cre-accent-button *ngIf="canEditTouchUpKits" routerLink="/misc/touch-up-kit/add">Ajouter</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<cre-table class="mx-auto" [dataSource]="uncompletedTouchUpKits$ | async" [columns]="columns">
<cre-table class="mx-auto" [data]="uncompletedTouchUpKits$ | async" [columns]="columns">
<ng-container matColumnDef="project">
<th mat-header-cell *matHeaderCellDef>Project</th>
<td mat-cell *matCellDef="let touchUpKit">{{touchUpKit.project}}</td>
@ -57,7 +57,7 @@
<mat-card-title>Kits de retouche complétés</mat-card-title>
</mat-card-header>
<mat-card-content>
<cre-table [dataSource]="completedTouchUpKits$ | async" [columns]="completedColumns">
<cre-table [data]="completedTouchUpKits$ | async" [columns]="completedColumns">
<ng-container matColumnDef="project">
<th mat-header-cell *matHeaderCellDef>Project</th>
<td mat-cell *matCellDef="let touchUpKit">{{touchUpKit.project}}</td>

View File

@ -10,7 +10,7 @@ import {Permission} from '../../shared/model/user'
import {RecipeService} from '../../recipes/services/recipe.service'
import {AppState} from '../../shared/app-state'
import {map} from 'rxjs/operators'
import {LocalDate, Period} from 'js-joda'
import {LocalDate, Period} from '@js-joda/core'
import {ConfigService} from '../../shared/service/config.service'
import {Config} from '../../shared/model/config.model'

View File

@ -1,21 +1,21 @@
// Custom Theming for Angular Material
@use '@angular/material' as mat;
// For more information: https://material.angular.io/guide/theming
@import '~@angular/material/theming';
// Plus imports for other components in your app.
$custom-typography: mat-typography-config(
$custom-typography: mat.define-typography-config(
$font-family: "Open Sans"
);
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat-core($custom-typography);
@include mat.core($custom-typography);
// Define the palettes for your theme using the Material Design palettes available in palette.sass
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$theme-primary: mat-palette((
$theme-primary: mat.define-palette((
50 : #e0e0e0,
100 : #b3b3b3,
200 : #808080,
@ -46,7 +46,7 @@ $theme-primary: mat-palette((
A400 : #ffffff,
A700 : #ffffff,
)));
$theme-accent: mat-palette((
$theme-accent: mat.define-palette((
50 : #edf9e0,
100 : #d1f0b3,
200 : #b3e680,
@ -78,7 +78,7 @@ $theme-accent: mat-palette((
A700 : #000000,
)
));
$theme-warning: mat-palette((
$theme-warning: mat.define-palette((
50 : #fff8e4,
100 : #feefbd,
200 : #fee491,
@ -112,15 +112,15 @@ $theme-warning: mat-palette((
));
// The warn palette is optional (defaults to red).
$theme-error: mat-palette($mat-red);
$theme-error: mat.define-palette(mat.$red-palette);
// Create the theme object (a Sass map containing all of the palettes).
$color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-accent, $theme-error);
$color-recipes-explorer-frontend-theme: mat.define-light-theme($theme-primary, $theme-accent, $theme-error);
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($color-recipes-explorer-frontend-theme);
@include mat.all-component-themes($color-recipes-explorer-frontend-theme);
html, body {

View File

@ -1,4 +1,5 @@
export const environment = {
production: true,
apiBaseUrl: window.location.origin,
apiUrl: window.location.origin + '/api'
};

View File

@ -4,6 +4,7 @@
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:9090',
apiUrl: 'http://localhost:9090/api'
};
@ -14,4 +15,4 @@ export const environment = {
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View File

@ -18,16 +18,6 @@
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
@ -55,7 +45,7 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************

View File

@ -1,6 +1,6 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
@ -17,7 +17,9 @@ declare const require: {
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
}
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);