Séparation du frontend et du backend de Color Recipes Explorer en deux projets
This commit is contained in:
commit
1907c71980
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,46 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
|
@ -0,0 +1,27 @@
|
|||
# ColorRecipesExplorerFrontend
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.5.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
|
@ -0,0 +1,132 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"color-recipes-explorer-frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "sass"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "cre",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/color-recipes-explorer-frontend",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{ "glob": "mdi.svg", "input": "./node_modules/@mdi/angular-material", "output": "./assets"}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||
"src/custom-theme.scss",
|
||||
"src/styles.sass"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "100kb",
|
||||
"maximumError": "200kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "color-recipes-explorer-frontend:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "color-recipes-explorer-frontend:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "color-recipes-explorer-frontend:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.sass"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "color-recipes-explorer-frontend:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "color-recipes-explorer-frontend:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "color-recipes-explorer-frontend"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
|
@ -0,0 +1,32 @@
|
|||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('color-recipes-explorer-frontend app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl) as Promise<unknown>;
|
||||
}
|
||||
|
||||
getTitleText(): Promise<string> {
|
||||
return element(by.css('cre-root .content span')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/color-recipes-explorer-frontend'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "color-recipes-explorer-frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~9.0.5",
|
||||
"@angular/cdk": "^9.2.4",
|
||||
"@angular/common": "~9.0.5",
|
||||
"@angular/compiler": "~9.0.5",
|
||||
"@angular/core": "~9.0.5",
|
||||
"@angular/forms": "~9.0.5",
|
||||
"@angular/material": "^9.2.4",
|
||||
"@angular/platform-browser": "~9.0.5",
|
||||
"@angular/platform-browser-dynamic": "~9.0.5",
|
||||
"@angular/router": "~9.0.5",
|
||||
"@mdi/angular-material": "^5.7.55",
|
||||
"bootstrap": "^4.5.2",
|
||||
"copy-webpack-plugin": "^6.2.1",
|
||||
"js-joda": "^1.11.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"ngx-material-file-input": "^2.1.1",
|
||||
"rxjs": "~6.5.4",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.900.5",
|
||||
"@angular/cli": "~9.0.5",
|
||||
"@angular/compiler-cli": "~9.0.5",
|
||||
"@angular/language-service": "~9.0.5",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/jasmine": "~3.5.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"codelyzer": "^5.1.2",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.1.0",
|
||||
"karma-jasmine": "~2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.2",
|
||||
"protractor": "~5.4.3",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~5.18.0",
|
||||
"typescript": "~3.7.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:9090/api",
|
||||
"secure": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {Routes, RouterModule} from '@angular/router';
|
||||
import {CatalogComponent} from "./pages/catalog/catalog.component";
|
||||
|
||||
|
||||
const routes: Routes = [{
|
||||
path: 'color',
|
||||
loadChildren: () => import('./modules/colors/colors.module').then(m => m.ColorsModule)
|
||||
}, {
|
||||
path: 'account',
|
||||
loadChildren: () => import('./modules/accounts/accounts.module').then(m => m.AccountsModule)
|
||||
}, {
|
||||
path: 'employee',
|
||||
loadChildren: () => import('./modules/employees/employees.module').then(m => m.EmployeesModule)
|
||||
}, {
|
||||
path: 'group',
|
||||
loadChildren: () => import('./modules/groups/groups.module').then(m => m.GroupsModule)
|
||||
}, {
|
||||
path: 'catalog',
|
||||
component: CatalogComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'materialtype',
|
||||
loadChildren: () => import('./modules/material-type/material-type.module').then(m => m.MaterialTypeModule),
|
||||
},
|
||||
{
|
||||
path: 'material',
|
||||
loadChildren: () => import('./modules/material/material.module').then(m => m.MaterialModule)
|
||||
},
|
||||
{
|
||||
path: 'company',
|
||||
loadChildren: () => import('./modules/company/company.module').then(m => m.CompanyModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'materialtype'
|
||||
}
|
||||
]
|
||||
},
|
||||
{path: 'material', loadChildren: () => import('./modules/material/material.module').then(m => m.MaterialModule)}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<cre-header></cre-header>
|
||||
<div>
|
||||
<div class="dark-background"></div>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<div class="offline-server-card-wrapper" [hidden]="isServerOnline">
|
||||
<div class="dark-background"></div>
|
||||
<mat-card class="x-centered y-centered">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Erreur de connexion</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>Le serveur est présentement hors ligne. Réessayez plus tard.</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="accent" (click)="reload()">Réessayer</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
.offline-server-card-wrapper
|
||||
position: fixed
|
||||
top: 0
|
||||
z-index: 100
|
||||
|
||||
.dark-background
|
||||
position: fixed
|
||||
top: 0
|
||||
opacity: .5
|
||||
|
||||
mat-card
|
||||
left: 50vw
|
||||
transform: translate(-50%, -50%)
|
|
@ -0,0 +1,41 @@
|
|||
import {Component, Inject, PLATFORM_ID} from '@angular/core';
|
||||
import {isPlatformBrowser} from "@angular/common";
|
||||
import {AppState} from "./modules/shared/app-state";
|
||||
import {Observable} from "rxjs";
|
||||
import {SubscribingComponent} from "./modules/shared/components/subscribing.component";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.sass']
|
||||
})
|
||||
export class AppComponent extends SubscribingComponent {
|
||||
isOnline: boolean
|
||||
isServerOnline = true
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: object,
|
||||
private appState: AppState,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.isOnline = isPlatformBrowser(this.platformId)
|
||||
super.ngOnInit();
|
||||
|
||||
this.subscribe(
|
||||
this.appState.serverOnline$,
|
||||
{
|
||||
next: online => this.isServerOnline = online
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
reload() {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {MatIconRegistry} from "@angular/material/icon";
|
||||
import {SharedModule} from "./modules/shared/shared.module";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {CatalogComponent} from './pages/catalog/catalog.component';
|
||||
import {CompanyModule} from './modules/company/company.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
CatalogComponent
|
||||
],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
SharedModule,
|
||||
BrowserAnimationsModule,
|
||||
CompanyModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) {
|
||||
matIconRegistry.addSvgIconSet(
|
||||
domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg')
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {Routes, RouterModule} from '@angular/router';
|
||||
|
||||
import {LoginComponent} from './pages/login/login.component';
|
||||
import {LogoutComponent} from "./pages/logout/logout.component";
|
||||
|
||||
const routes: Routes = [{path: 'login', component: LoginComponent}, {path: 'logout', component: LogoutComponent}, {path: '', redirectTo: 'login'}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AccountsRoutingModule {
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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";
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [LoginComponent, LogoutComponent],
|
||||
imports: [
|
||||
SharedModule,
|
||||
AccountsRoutingModule,
|
||||
]
|
||||
})
|
||||
export class AccountsModule {
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<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>
|
||||
<div *ngIf="invalidCredentials" class="alert alert-danger">
|
||||
<p>Les identifiants entrés sont invalides.</p>
|
||||
</div>
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'employé</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'employé est requis</span>
|
||||
<span *ngIf="idFormControl.errors.pattern">Le numéro d'employé 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>
|
|
@ -0,0 +1,8 @@
|
|||
mat-card
|
||||
width: 25rem
|
||||
|
||||
.alert p
|
||||
margin: 0
|
||||
|
||||
mat-form-field
|
||||
width: 100%
|
|
@ -0,0 +1,46 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {AccountService} from "../../services/account.service";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.sass']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
form: FormGroup
|
||||
idFormControl: FormControl
|
||||
passwordFormControl: FormControl
|
||||
|
||||
invalidCredentials = false
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
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"]),
|
||||
err => this.invalidCredentials = err.status === 401
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {AccountService} from "../../services/account.service";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-logout',
|
||||
templateUrl: './logout.component.html',
|
||||
styleUrls: ['./logout.component.sass']
|
||||
})
|
||||
export class LogoutComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.accountService.isLoggedIn()) {
|
||||
this.router.navigate(['/account/login'])
|
||||
}
|
||||
|
||||
this.accountService.logout(() => {
|
||||
this.router.navigate(['/account/login'])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {Subject} from "rxjs";
|
||||
import {take, takeUntil} from "rxjs/operators";
|
||||
import {AppState} from "../../shared/app-state";
|
||||
import {HttpClient, HttpResponse} from "@angular/common/http";
|
||||
import {environment} from "../../../../environments/environment";
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {Employee, EmployeePermission} from "../../shared/model/employee";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountService implements OnDestroy {
|
||||
private destroy$ = new Subject<boolean>()
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private api: ApiService,
|
||||
private appState: AppState
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(true)
|
||||
this.destroy$.complete()
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.appState.isAuthenticated
|
||||
}
|
||||
|
||||
checkAuthenticationStatus() {
|
||||
if (!this.appState.authenticatedEmployee) {
|
||||
// Try to get current default group user
|
||||
this.http.get<Employee>(`${environment.apiUrl}/employee/current`, {withCredentials: true})
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe({
|
||||
next: employee => this.appState.authenticatedEmployee = employee,
|
||||
error: err => {
|
||||
if (err.status === 0 && err.statusText === "Unknown Error") {
|
||||
this.appState.isServerOnline = false
|
||||
} else {
|
||||
this.appState.isServerOnline = true
|
||||
if (err.status === 404 || err.status === 403) {
|
||||
console.error('No default user is defined on this computer')
|
||||
} else {
|
||||
console.error('An error occurred while authenticating the default user')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
login(id: number, password: string, success: () => void, error: (err) => void) {
|
||||
const loginForm = {id, password}
|
||||
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
|
||||
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.setLoggedInEmployeeFromApi()
|
||||
success()
|
||||
},
|
||||
error: err => {
|
||||
if (err.status === 0 && err.statusText === "Unknown Error") {
|
||||
this.appState.isServerOnline = false
|
||||
} else {
|
||||
this.appState.isServerOnline = true
|
||||
error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logout(success: () => void) {
|
||||
this.api.get<void>('/employee/logout', true).pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.appState.isAuthenticated = false
|
||||
this.appState.authenticationExpiration = -1
|
||||
this.appState.authenticatedEmployee = null
|
||||
this.checkAuthenticationStatus()
|
||||
success()
|
||||
},
|
||||
error: err => console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
hasPermission(permission: EmployeePermission): boolean {
|
||||
return this.appState.authenticatedEmployee && this.appState.authenticatedEmployee.permissions.indexOf(permission) >= 0
|
||||
}
|
||||
|
||||
private setLoggedInEmployeeFromApi() {
|
||||
this.api.get<Employee>("/employee/current", true)
|
||||
.pipe(
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: employee => this.appState.authenticatedEmployee = employee,
|
||||
error: err => {
|
||||
console.error("Could not get the logged in employee from the API: ")
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,39 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {ListComponent} from "./pages/list/list.component";
|
||||
import {AddComponent} from "./pages/add/add.component";
|
||||
import {EditComponent} from "./pages/edit/edit.component";
|
||||
import {ExploreComponent} from "./pages/explore/explore.component";
|
||||
import {MixEditComponent} from "./pages/mix/mix-edit/mix-edit.component";
|
||||
import {MixAddComponent} from "./pages/mix/mix-add/mix-add.component";
|
||||
|
||||
const routes: Routes = [{
|
||||
path: 'list',
|
||||
component: ListComponent
|
||||
}, {
|
||||
path: 'add',
|
||||
component: AddComponent
|
||||
}, {
|
||||
path: 'edit/:id',
|
||||
component: EditComponent
|
||||
}, {
|
||||
path: 'add/mix/:recipeId',
|
||||
component: MixAddComponent
|
||||
}, {
|
||||
path: 'edit/mix/:recipeId/:id',
|
||||
component: MixEditComponent
|
||||
}, {
|
||||
path: 'explore/:id',
|
||||
component: ExploreComponent
|
||||
}, {
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'list'
|
||||
}]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ColorsRoutingModule {
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {ColorsRoutingModule} from './colors-routing.module';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {ListComponent} from './pages/list/list.component';
|
||||
import {AddComponent} from './pages/add/add.component';
|
||||
import {EditComponent} from './pages/edit/edit.component';
|
||||
import {MatExpansionModule} from "@angular/material/expansion";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {ExploreComponent} from './pages/explore/explore.component';
|
||||
import {RecipeInfoComponent} from './components/recipe-info/recipe-info.component';
|
||||
import {MixTableComponent} from './components/mix-table/mix-table.component';
|
||||
import {StepListComponent} from './components/step-list/step-list.component';
|
||||
import {StepTableComponent} from './components/step-table/step-table.component';
|
||||
import {MixEditorComponent} from './components/mix-editor/mix-editor.component';
|
||||
import {UnitSelectorComponent} from './components/unit-selector/unit-selector.component';
|
||||
import {MixAddComponent} from './pages/mix/mix-add/mix-add.component';
|
||||
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component';
|
||||
import { ImagesEditorComponent } from './components/images-editor/images-editor.component';
|
||||
import { MixesCardComponent } from './components/mixes-card/mixes-card.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent, ImagesEditorComponent, MixesCardComponent],
|
||||
imports: [
|
||||
ColorsRoutingModule,
|
||||
SharedModule,
|
||||
MatExpansionModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class ColorsModule {
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Images</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content [class.no-action]="!editionMode">
|
||||
<div class="d-flex flex-row justify-content-around flex-wrap">
|
||||
<div *ngFor="let imageId of (imageIds$ | async)" class="d-flex flex-column align-self-center m-3">
|
||||
<div class="image-wrapper">
|
||||
<img src="{{backendUrl}}/recipe/{{recipe.id}}/image/{{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(imageId)">Afficher</button>
|
||||
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageId)">Retirer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions *ngIf="editionMode">
|
||||
<cre-file-button
|
||||
color="accent"
|
||||
label="Ajouter"
|
||||
accept="image/jpeg,image/png"
|
||||
(change)="submit($event)">
|
||||
</cre-file-button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,9 @@
|
|||
mat-card
|
||||
background-color: rgba(255, 255, 255, 0.5)
|
||||
max-width: 90vw !important
|
||||
|
||||
.image-wrapper
|
||||
padding: 16px
|
||||
border-radius: 4px
|
||||
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12)
|
||||
background-color: white
|
|
@ -0,0 +1,57 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {Recipe} from "../../../shared/model/recipe.model";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Observable} from "rxjs";
|
||||
import {RecipeImageService} from "../../services/recipe-image.service";
|
||||
import {environment} from "../../../../../environments/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-images-editor',
|
||||
templateUrl: './images-editor.component.html',
|
||||
styleUrls: ['./images-editor.component.sass']
|
||||
})
|
||||
export class ImagesEditorComponent extends SubscribingComponent {
|
||||
@Input() recipe: Recipe
|
||||
@Input() editionMode = false
|
||||
|
||||
imageIds$: Observable<number[]>
|
||||
backendUrl = environment.apiUrl
|
||||
|
||||
constructor(
|
||||
private recipeImageService: RecipeImageService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit()
|
||||
|
||||
this.loadImagesIds()
|
||||
}
|
||||
|
||||
submit(event) {
|
||||
const image = event.target.files[0]
|
||||
this.subscribe(
|
||||
this.recipeImageService.save(image, this.recipe.id),
|
||||
{next: () => this.loadImagesIds()}
|
||||
)
|
||||
}
|
||||
|
||||
openImage(imageId: number) {
|
||||
window.open(`${environment.apiUrl}/recipe/${this.recipe.id}/image/${imageId}`, '_blank')
|
||||
}
|
||||
|
||||
delete(imageId: number) {
|
||||
this.subscribe(
|
||||
this.recipeImageService.delete(imageId, this.recipe.id),
|
||||
{next: () => this.loadImagesIds()}
|
||||
)
|
||||
}
|
||||
|
||||
private loadImagesIds() {
|
||||
this.imageIds$ = this.recipeImageService.getAllIdsForRecipe(this.recipe.id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
<mat-card *ngIf="recipe && (!editionMode || mix)" class="x-centered y-centered">
|
||||
<mat-card-header>
|
||||
<mat-card-title *ngIf="!editionMode">Création d'un mélange pour la recette {{recipe.company.name}}
|
||||
- {{recipe.name}}</mat-card-title>
|
||||
<mat-card-title *ngIf="editionMode">Modification du mélange {{mix.mixType.name}} de la
|
||||
recette {{recipe.company.name}} - {{recipe.name}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="nameControl"/>
|
||||
<mat-icon svgIcon="form-textbox" matSuffix></mat-icon>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Type de produit</mat-label>
|
||||
<mat-select [formControl]="materialTypeControl">
|
||||
<mat-option
|
||||
*ngFor="let materialType of (materialTypes$ | async)"
|
||||
[value]="materialType.id">
|
||||
{{materialType.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="mix-materials-wrapper">
|
||||
<ng-container *ngTemplateOutlet="mixEditor"></ng-container>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/color/edit/{{recipeId}}">Retour</button>
|
||||
<button *ngIf="editionMode && canDeleteMix" mat-raised-button color="warn" (click)="deleteConfirmBox.show()">Supprimer</button>
|
||||
<button mat-raised-button color="accent" [disabled]="!form.valid" (click)="submit()">Enregistrer</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<ng-template #mixEditor>
|
||||
<table #mixTable mat-table [dataSource]="mixMaterials">
|
||||
<ng-container matColumnDef="position">
|
||||
<th mat-header-cell *matHeaderCellDef>Position</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial; let i = index">
|
||||
{{i + 1}}
|
||||
</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 [(ngModel)]="mixMaterial.materialId">
|
||||
<mat-option *ngFor="let material of materials"
|
||||
[value]="material.id">{{material.name}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="quantity">
|
||||
<th mat-header-cell *matHeaderCellDef>Quantité</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial">
|
||||
<mat-form-field>
|
||||
<input matInput type="number" step="0.001" [(ngModel)]="mixMaterial.quantity"/>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="units">
|
||||
<th mat-header-cell *matHeaderCellDef>Unités</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial" class="units-wrapper">
|
||||
<ng-container *ngIf="materials">
|
||||
<ng-container *ngIf="materialUsePercentages(mixMaterial)">
|
||||
<p>%</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!materialUsePercentages(mixMaterial)">
|
||||
<ng-container *ngIf="!hoveredMixMaterial || hoveredMixMaterial != mixMaterial">
|
||||
<span>{{units}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hoveredMixMaterial && hoveredMixMaterial == mixMaterial">
|
||||
<cre-unit-selector [(unit)]="units" [showLabel]="false" [short]="true"></cre-unit-selector>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="buttonRemove">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<button mat-raised-button color="accent" (click)="addRow()">Ajouter</button>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial; let i = index">
|
||||
<ng-container *ngIf="hoveredMixMaterial && hoveredMixMaterial == mixMaterial">
|
||||
<button mat-raised-button color="warn" (click)="removeRow(i)">Retirer</button>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr mat-row *matRowDef="let mixMaterial; columns: columns" (mouseover)="hoveredMixMaterial = mixMaterial"></tr>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<cre-confirm-box
|
||||
#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>
|
|
@ -0,0 +1,3 @@
|
|||
td.units-wrapper p
|
||||
width: 3rem
|
||||
margin-bottom: 0
|
|
@ -0,0 +1,143 @@
|
|||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {Mix, MixMaterial, Recipe} from "../../../shared/model/recipe.model";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {MixService} from "../../services/mix.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {RecipeService} from "../../services/recipe.service";
|
||||
import {Material} from "../../../shared/model/material.model";
|
||||
import {MaterialService} from "../../../material/service/material.service";
|
||||
import {MaterialType} from "../../../shared/model/materialtype.model";
|
||||
import {MaterialTypeService} from "../../../material-type/service/material-type.service";
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {UNIT_MILLILITER} from "../../../shared/units";
|
||||
import {MatTable} from "@angular/material/table";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {ConfirmBoxComponent} from "../../../shared/components/confirm-box/confirm-box.component";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {EmployeePermission} from "../../../shared/model/employee";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-mix-editor',
|
||||
templateUrl: './mix-editor.component.html',
|
||||
styleUrls: ['./mix-editor.component.sass']
|
||||
})
|
||||
export class MixEditorComponent extends SubscribingComponent {
|
||||
@ViewChild('mixTable') mixTable: MatTable<MixMaterial>
|
||||
@ViewChild('deleteConfirmBox') deleteConfirmBox: ConfirmBoxComponent
|
||||
|
||||
@Input() mixId: number | null
|
||||
@Input() recipeId: number | null
|
||||
@Input() materials: Material[]
|
||||
|
||||
@Output() save = new EventEmitter<any>();
|
||||
|
||||
mix: Mix | null
|
||||
recipe: Recipe | null
|
||||
materialTypes$: Observable<MaterialType[]>
|
||||
|
||||
form: FormGroup
|
||||
nameControl: FormControl
|
||||
materialTypeControl: FormControl
|
||||
|
||||
mixMaterials = []
|
||||
editionMode = false
|
||||
units = UNIT_MILLILITER
|
||||
hoveredMixMaterial: MixMaterial | null
|
||||
columns = ['position', 'material', 'quantity', 'units', 'buttonRemove']
|
||||
|
||||
constructor(
|
||||
private mixService: MixService,
|
||||
private recipeService: RecipeService,
|
||||
private materialService: MaterialService,
|
||||
private materialTypeService: MaterialTypeService,
|
||||
private accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.mixId = this.urlUtils.parseIntUrlParam('id')
|
||||
if (this.mixId) {
|
||||
this.editionMode = true
|
||||
}
|
||||
|
||||
this.subscribe(
|
||||
this.recipeService.getById(this.recipeId),
|
||||
{
|
||||
next: r => {
|
||||
this.recipe = r
|
||||
if (this.editionMode) {
|
||||
this.subscribe(
|
||||
this.mixService.getById(this.mixId),
|
||||
{
|
||||
next: m => {
|
||||
this.mix = m
|
||||
this.mixMaterials = this.mixService.extractMixMaterials(this.mix)
|
||||
this.generateForm()
|
||||
}, error: err => this.handleNotFoundError(err, '/color/list')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.mixMaterials.push({})
|
||||
this.generateForm()
|
||||
}
|
||||
},
|
||||
error: err => this.handleNotFoundError(err, '/color/list')
|
||||
}
|
||||
)
|
||||
this.materialTypes$ = this.materialTypeService.all
|
||||
}
|
||||
|
||||
addRow() {
|
||||
this.mixMaterials.push({materialId: null, quantity: null, percents: false})
|
||||
this.mixTable.renderRows()
|
||||
}
|
||||
|
||||
removeRow(position: number) {
|
||||
this.mixMaterials.splice(position, 1)
|
||||
this.mixTable.renderRows()
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.save.emit({
|
||||
name: this.nameControl.value,
|
||||
recipeId: this.recipeId,
|
||||
materialTypeId: this.materialTypeControl.value,
|
||||
mixMaterials: this.mixMaterials,
|
||||
units: this.units
|
||||
})
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribeAndNavigate(this.mixService.delete(this.mixId), `/color/edit/${this.recipeId}`)
|
||||
}
|
||||
|
||||
materialUsePercentages(mixMaterial: any) {
|
||||
if (!mixMaterial.materialId) return null
|
||||
const material = this.getMaterialFromId(mixMaterial.materialId);
|
||||
mixMaterial.percents = material && material.materialType.usePercentages
|
||||
return mixMaterial.percents
|
||||
}
|
||||
|
||||
getMaterialFromId(id: number): Material {
|
||||
return id ? this.materials.filter(m => m.id === id)[0] : null
|
||||
}
|
||||
|
||||
get canDeleteMix() {
|
||||
return this.accountService.hasPermission(EmployeePermission.REMOVE_RECIPE)
|
||||
}
|
||||
|
||||
private generateForm() {
|
||||
this.nameControl = new FormControl(this.mix ? this.mix.mixType.name : null, Validators.required)
|
||||
this.materialTypeControl = new FormControl(this.mix ? this.mix.mixType.material.materialType.id : null, Validators.required)
|
||||
this.form = this.formBuilder.group({
|
||||
name: this.nameControl,
|
||||
materialType: this.materialTypeControl
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
<mat-expansion-panel class="table-title mt-5">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{mix.mixType.name}}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="mix-actions d-flex flex-row justify-content-between">
|
||||
<ng-container *ngIf="!editionMode">
|
||||
<div class="flex-grow-1">
|
||||
<mat-form-field class="dark">
|
||||
<mat-label>Casier</mat-label>
|
||||
<input matInput type="text" [(ngModel)]="mix.location" (change)="changeLocation($event)"/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-raised-button color="accent" (click)="printingConfirmBox.show()">Imprimer</button>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-raised-button color="accent" (click)="deduct.emit()">Déduire</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="editionMode">
|
||||
<div class="flex-grow-1"></div>
|
||||
<button mat-raised-button color="accent" routerLink="/color/edit/mix/{{recipe.id}}/{{mix.id}}">Modifier</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="mixTable"></ng-container>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<ng-template #mixTable>
|
||||
<table mat-table [dataSource]="mix.mixMaterials">
|
||||
<ng-container matColumnDef="material">
|
||||
<th mat-header-cell *matHeaderCellDef>Produit</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.material.name}}</td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="materialType">
|
||||
<th mat-header-cell *matHeaderCellDef>Type</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial">{{mixMaterial.material.materialType.name}}</td>
|
||||
<td mat-footer-cell *matFooterCellDef></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"
|
||||
min="0.001"
|
||||
step="0.001"
|
||||
[value]="getComputedQuantityRounded(mixMaterial)"
|
||||
[disabled]="mixMaterial.material.materialType.usePercentages"
|
||||
(change)="changeQuantity($event, mixMaterial)"/>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td mat-footer-cell *matFooterCellDef>
|
||||
<mat-form-field>
|
||||
<mat-label>Total</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
min="0.001"
|
||||
step="0.001"
|
||||
[value]="round(getTotalQuantity())"
|
||||
(change)="changeQuantity($event, null, true)"/>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="quantityStatic">
|
||||
<th mat-header-cell *matHeaderCellDef>Quantité</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial">{{getComputedQuantityRounded(mixMaterial)}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="quantityCalculated">
|
||||
<th mat-header-cell *matHeaderCellDef>Calcul</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial; let i = index" [innerHTML]="getCalculatedQuantity(mixMaterial, i)"
|
||||
class="mix-calculation"></td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="quantityUnits">
|
||||
<th mat-header-cell *matHeaderCellDef>Unités</th>
|
||||
<td mat-cell *matCellDef="let mixMaterial">
|
||||
<ng-container *ngIf="mixMaterial.material.materialType.usePercentages">%</ng-container>
|
||||
<ng-container *ngIf="!mixMaterial.material.materialType.usePercentages">{{units}}</ng-container>
|
||||
</td>
|
||||
<td mat-footer-cell *matFooterCellDef>{{units}}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="mixColumns"></tr>
|
||||
<tr mat-row *matRowDef="let mixMaterial; columns: mixColumns"
|
||||
[class.low-quantity]="!editionMode && isInLowQuantity(mixMaterial.id)"></tr>
|
||||
<ng-container *ngIf="!editionMode">
|
||||
<tr mat-footer-row *matFooterRowDef="mixColumns"></tr>
|
||||
</ng-container>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<cre-confirm-box
|
||||
#printingConfirmBox
|
||||
message="Voulez-vous vraiment imprimer ce mélange?"
|
||||
(confirm)="print()">
|
||||
</cre-confirm-box>
|
|
@ -0,0 +1,22 @@
|
|||
@import '../../../../../custom-theme'
|
||||
|
||||
mat-expansion-panel
|
||||
width: 40rem
|
||||
margin: 2rem 0
|
||||
|
||||
.mix-actions
|
||||
background-color: $color-primary
|
||||
padding: 0 1rem
|
||||
|
||||
div:last-child
|
||||
margin-left: 1rem
|
||||
|
||||
.low-quantity
|
||||
background-color: #ffb3b3
|
||||
|
||||
::ng-deep span.mix-calculated-quantity
|
||||
&:first-child
|
||||
color: green
|
||||
|
||||
&:last-child
|
||||
color: dimgrey
|
|
@ -0,0 +1,160 @@
|
|||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {Mix, MixMaterial, Recipe} from "../../../shared/model/recipe.model";
|
||||
import {Subject} from "rxjs";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {convertMixMaterialQuantity, UNIT_MILLILITER} from "../../../shared/units";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {PtouchPrinter} from "../../ptouchPrint"
|
||||
import {ConfirmBoxComponent} from "../../../shared/components/confirm-box/confirm-box.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-mix-table',
|
||||
templateUrl: './mix-table.component.html',
|
||||
styleUrls: ['./mix-table.component.sass']
|
||||
})
|
||||
export class MixTableComponent extends SubscribingComponent {
|
||||
private readonly COLUMNS = ['material', 'materialType', 'quantity', 'quantityCalculated', 'quantityUnits']
|
||||
private readonly COLUMNS_STATIC = ['material', 'materialType', 'quantityStatic', 'quantityUnits']
|
||||
|
||||
@ViewChild('printingConfirmBox') printingConfirmBox: ConfirmBoxComponent
|
||||
|
||||
@Input() mix: Mix
|
||||
@Input() recipe: Recipe
|
||||
@Input() units$: Subject<string>
|
||||
@Input() deductErrorBody
|
||||
@Input() editionMode: boolean
|
||||
@Input() printingError = 2
|
||||
@Output() locationChange = new EventEmitter<{ id: number, location: string }>()
|
||||
@Output() quantityChange = new EventEmitter<{ id: number, materialId: number, quantity: number }>()
|
||||
@Output() deduct = new EventEmitter<void>()
|
||||
@Output() printingErrorChange = new EventEmitter<number>()
|
||||
|
||||
mixColumns = this.COLUMNS
|
||||
units = UNIT_MILLILITER
|
||||
computedQuantities: { id: number, percents: boolean, quantity: number }[] = []
|
||||
|
||||
// BPac printer
|
||||
printer: PtouchPrinter | null
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
if (this.editionMode) {
|
||||
this.mixColumns = this.COLUMNS_STATIC
|
||||
}
|
||||
|
||||
this.mix.mixMaterials.forEach(m => this.computedQuantities.push({
|
||||
id: m.id,
|
||||
percents: m.material.materialType.usePercentages,
|
||||
quantity: m.quantity
|
||||
}))
|
||||
|
||||
this.subscribe(
|
||||
this.units$,
|
||||
{
|
||||
next: u => this.convertQuantities(u)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
changeLocation(event: any) {
|
||||
this.locationChange.emit({id: this.mix.id, location: event.target.value})
|
||||
}
|
||||
|
||||
changeQuantity(event: any, mixMaterial: MixMaterial, isTotal = false) {
|
||||
const newQuantity = parseInt(event.target.value)
|
||||
let ratio = 1
|
||||
if (!isTotal) {
|
||||
const originalQuantity = this.getComputedQuantity(mixMaterial.id)
|
||||
ratio = newQuantity / originalQuantity.quantity
|
||||
} else {
|
||||
ratio = newQuantity / this.getTotalQuantity()
|
||||
}
|
||||
this.computedQuantities.forEach((q, i) => {
|
||||
if (!q.percents) {
|
||||
q.quantity *= ratio
|
||||
}
|
||||
this.emitQuantityChangeEvent(i)
|
||||
})
|
||||
}
|
||||
|
||||
getComputedQuantityRounded(mixMaterial: MixMaterial): number {
|
||||
return this.round(this.getComputedQuantity(mixMaterial.id).quantity)
|
||||
}
|
||||
|
||||
getTotalQuantity(index: number = -1): number {
|
||||
if (index === -1) index = this.computedQuantities.length - 1
|
||||
let totalQuantity = 0
|
||||
for (let i = 0; i <= index; i++) {
|
||||
totalQuantity += this.calculateQuantity(i)
|
||||
}
|
||||
return totalQuantity
|
||||
}
|
||||
|
||||
getCalculatedQuantity(mixMaterial: MixMaterial, index: number): string {
|
||||
const totalQuantity = this.round(this.getTotalQuantity(index))
|
||||
const addedQuantity = this.round(this.calculateQuantity(index))
|
||||
return `<span class="mix-calculated-quantity">+${addedQuantity}</span> <span class="mix-calculated-quantity">(${totalQuantity})</span>`
|
||||
}
|
||||
|
||||
isInLowQuantity(materialId: number): boolean {
|
||||
return this.deductErrorBody[this.mix.id] && this.deductErrorBody[this.mix.id].indexOf(materialId) >= 0
|
||||
}
|
||||
|
||||
round(quantity: number): number {
|
||||
return Math.round(quantity * 1000) / 1000
|
||||
}
|
||||
|
||||
async print() {
|
||||
const base = this.mix.mixMaterials
|
||||
.map(ma => ma.material)
|
||||
.filter(m => m.materialType.name === 'Base')[0]
|
||||
if (!base) {
|
||||
this.printingErrorChange.emit(98)
|
||||
return
|
||||
}
|
||||
this.printer = new PtouchPrinter({
|
||||
template: "Couleur",
|
||||
lines: [
|
||||
{name: "color", value: this.recipe.name},
|
||||
{name: "banner", value: this.recipe.company.name},
|
||||
{name: "base", value: base.name},
|
||||
{name: "description", value: this.recipe.description}
|
||||
]
|
||||
})
|
||||
const errorCode = await this.printer.print()
|
||||
this.printingErrorChange.emit(errorCode)
|
||||
}
|
||||
|
||||
private emitQuantityChangeEvent(index: number) {
|
||||
this.quantityChange.emit({
|
||||
id: this.mix.id,
|
||||
materialId: this.computedQuantities[index].id,
|
||||
quantity: this.calculateQuantity(index)
|
||||
})
|
||||
}
|
||||
|
||||
private convertQuantities(newUnit: string) {
|
||||
this.computedQuantities.forEach(q => q.quantity = convertMixMaterialQuantity(q, this.units, newUnit))
|
||||
this.units = newUnit
|
||||
}
|
||||
|
||||
private getComputedQuantity(id: number): any {
|
||||
return this.computedQuantities.filter(q => q.id == id)[0]
|
||||
}
|
||||
|
||||
private calculateQuantity(index: number): number {
|
||||
const computedQuantity = this.computedQuantities[index]
|
||||
if (!computedQuantity.percents) {
|
||||
return computedQuantity.quantity
|
||||
}
|
||||
return this.computedQuantities[0].quantity * (computedQuantity.quantity / 100)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Mélanges</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content [class.no-action]="!editionMode">
|
||||
<ng-container *ngFor="let mix of recipe.mixes; let i = index">
|
||||
<cre-mix-table
|
||||
[class.no-top-margin]="i == 0"
|
||||
[recipe]="recipe"
|
||||
[mix]="mix"
|
||||
[units$]="units$"
|
||||
[deductErrorBody]="deductErrorBody"
|
||||
[editionMode]="editionMode"
|
||||
(quantityChange)="quantityChange.emit($event)"
|
||||
(locationChange)="locationChange.emit($event)"
|
||||
(deduct)="deduct.emit(mix.id)"
|
||||
[(printingError)]="printingError">>
|
||||
</cre-mix-table>
|
||||
</ng-container>
|
||||
</mat-card-content>
|
||||
<mat-card-actions *ngIf="editionMode">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
routerLink="/color/add/mix/{{recipe.id}}">
|
||||
Ajouter
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,3 @@
|
|||
mat-card
|
||||
background-color: rgba(255, 255, 255, 0.5)
|
||||
min-width: 20rem
|
|
@ -0,0 +1,21 @@
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {Recipe} from "../../../shared/model/recipe.model";
|
||||
import {Subject} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-mixes-card',
|
||||
templateUrl: './mixes-card.component.html',
|
||||
styleUrls: ['./mixes-card.component.sass']
|
||||
})
|
||||
export class MixesCardComponent {
|
||||
@Input() recipe: Recipe
|
||||
@Input() units$: Subject<string>
|
||||
@Input() deductErrorBody: any
|
||||
@Input() printingError = 2
|
||||
@Input() editionMode = false
|
||||
|
||||
@Output() locationChange = new EventEmitter<{ id: number, location: string }>()
|
||||
@Output() quantityChange = new EventEmitter<{ id: number, materialId: number, quantity: number }>()
|
||||
@Output() deduct = new EventEmitter<number>()
|
||||
@Output() printingErrorChange = new EventEmitter<number>()
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<div class="recipe-info-wrapper d-flex flex-column">
|
||||
<div class="d-flex flex-column">
|
||||
<h3>{{recipe.company.name}} - {{recipe.name}}</h3>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-column">
|
||||
<p>Échantillon #{{recipe.sample}}</p>
|
||||
<p *ngIf="recipe.approbationDate">Approuvée le {{recipe.approbationDate}}</p>
|
||||
<div *ngIf="!recipe.approbationDate" class="recipe-not-approved-wrapper d-flex flex-row">
|
||||
<p>Non approuvée</p>
|
||||
<mat-icon svgIcon="alert" class="color-warning"></mat-icon>
|
||||
</div>
|
||||
<p>{{recipe.remark}}</p>
|
||||
</div>
|
||||
<div class="recipe-description">
|
||||
<p>{{recipe.description}}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1"></div>
|
||||
|
||||
<mat-icon
|
||||
*ngIf="hasModifications"
|
||||
class="color-warning has-modification-icon mr-4"
|
||||
svgIcon="pencil"
|
||||
title="Les modifications apportées n'ont pas été enregistrées"
|
||||
[inline]="true">
|
||||
</mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="!isBPacExtensionInstalled"
|
||||
color="warn"
|
||||
svgIcon="printer-alert"
|
||||
title="L'extension b-Pac n'est pas installée"
|
||||
[inline]="true">
|
||||
</mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="isBPacExtensionInstalled"
|
||||
color="accent"
|
||||
svgIcon="printer"
|
||||
[inline]="true">
|
||||
</mat-icon>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,46 @@
|
|||
.recipe-info-wrapper
|
||||
background-color: black
|
||||
color: white
|
||||
padding: 1rem
|
||||
|
||||
div
|
||||
margin-right: 3rem
|
||||
|
||||
p
|
||||
margin-bottom: 0
|
||||
|
||||
h3
|
||||
font-weight: bold
|
||||
text-decoration: underline
|
||||
text-transform: uppercase
|
||||
|
||||
&.recipe-not-approved-wrapper
|
||||
p
|
||||
margin-top: 1px
|
||||
margin-right: .4em
|
||||
|
||||
&.recipe-description
|
||||
max-width: 30rem
|
||||
|
||||
&.recipe-note
|
||||
margin-right: 0
|
||||
|
||||
mat-form-field
|
||||
width: 100%
|
||||
|
||||
::ng-deep .mat-form-field-wrapper
|
||||
padding-bottom: 0
|
||||
|
||||
::ng-deep .mat-form-field-underline, ::ng-deep .mat-form-field-ripple
|
||||
opacity: 0
|
||||
|
||||
mat-label
|
||||
color: white
|
||||
|
||||
textarea:focus
|
||||
background-color: white
|
||||
color: black
|
||||
|
||||
mat-icon
|
||||
width: 2em !important
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import {AfterViewInit, Component, Input} from '@angular/core';
|
||||
import {Recipe} from "../../../shared/model/recipe.model";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-recipe-info',
|
||||
templateUrl: './recipe-info.component.html',
|
||||
styleUrls: ['./recipe-info.component.sass']
|
||||
})
|
||||
export class RecipeInfoComponent implements AfterViewInit {
|
||||
@Input() recipe: Recipe
|
||||
@Input() hasModifications: boolean
|
||||
|
||||
isBPacExtensionInstalled = false
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.isBPacExtensionInstalled = document.querySelectorAll(".bpac-extension-installed").length > 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Étapes</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="no-action">
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let step of steps;let i = index">
|
||||
{{i + 1}}.<span class="space"></span>{{step.message}}
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,2 @@
|
|||
.space
|
||||
width: 1em
|
|
@ -0,0 +1,11 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {RecipeStep} from "../../../shared/model/recipe.model";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-step-list',
|
||||
templateUrl: './step-list.component.html',
|
||||
styleUrls: ['./step-list.component.sass']
|
||||
})
|
||||
export class StepListComponent {
|
||||
@Input() steps: RecipeStep[]
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<mat-expansion-panel class="table-title" [expanded]="true" [disabled]="true">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>Étapes</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<table #stepTable mat-table [dataSource]="steps">
|
||||
<ng-container matColumnDef="position">
|
||||
<th mat-header-cell *matHeaderCellDef>Position</th>
|
||||
<td mat-cell *matCellDef="let step; let i = index">{{i + 1}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="message">
|
||||
<th mat-header-cell *matHeaderCellDef>Message</th>
|
||||
<td mat-cell *matCellDef="let step">
|
||||
<mat-form-field>
|
||||
<input matInput type="text" [(ngModel)]="step.message"/>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="buttonRemove">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<button mat-raised-button color="accent" (click)="addStep()">Ajouter</button>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let step; let i = index">
|
||||
<button mat-raised-button color="warn" (click)="removeStep(i)">Retirer</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr mat-row *matRowDef="let step; columns: columns"></tr>
|
||||
</table>
|
||||
</mat-expansion-panel>
|
|
@ -0,0 +1,5 @@
|
|||
mat-expansion-panel
|
||||
min-width: 560px
|
||||
|
||||
mat-form-field
|
||||
width: 20rem
|
|
@ -0,0 +1,25 @@
|
|||
import {Component, Input, ViewChild} from '@angular/core';
|
||||
import {RecipeStep} from "../../../shared/model/recipe.model";
|
||||
import {MatTable} from "@angular/material/table";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-step-table',
|
||||
templateUrl: './step-table.component.html',
|
||||
styleUrls: ['./step-table.component.sass']
|
||||
})
|
||||
export class StepTableComponent {
|
||||
@ViewChild('stepTable', {static: true}) stepTable: MatTable<RecipeStep>
|
||||
readonly columns = ['position', 'message', 'buttonRemove']
|
||||
|
||||
@Input() steps: RecipeStep[]
|
||||
|
||||
addStep() {
|
||||
this.steps.push({id: null, message: ""})
|
||||
this.stepTable.renderRows()
|
||||
}
|
||||
|
||||
removeStep(position: number) {
|
||||
this.steps.splice(position, 1)
|
||||
this.stepTable.renderRows()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<mat-form-field [class.short]="short">
|
||||
<mat-label *ngIf="showLabel">Unités</mat-label>
|
||||
<mat-select [value]="unit" (selectionChange)="unitChange.emit($event.value)">
|
||||
<ng-container *ngIf="!short">
|
||||
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
|
||||
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
|
||||
<mat-option [value]="unitConstants.UNIT_GALLON">Gallons</mat-option>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="short">
|
||||
<mat-option [value]="unitConstants.UNIT_MILLILITER">mL</mat-option>
|
||||
<mat-option [value]="unitConstants.UNIT_LITER">L</mat-option>
|
||||
<mat-option [value]="unitConstants.UNIT_GALLON">gal</mat-option>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
|
@ -0,0 +1,2 @@
|
|||
mat-form-field.short
|
||||
width: 3rem
|
|
@ -0,0 +1,17 @@
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-unit-selector',
|
||||
templateUrl: './unit-selector.component.html',
|
||||
styleUrls: ['./unit-selector.component.sass']
|
||||
})
|
||||
export class UnitSelectorComponent {
|
||||
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
|
||||
|
||||
@Input() unit = UNIT_MILLILITER
|
||||
@Input() showLabel = true
|
||||
@Input() short = false
|
||||
|
||||
@Output() unitChange = new EventEmitter<string>()
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<cre-entity-add
|
||||
title="Création d'une recette"
|
||||
backButtonLink="/color/list"
|
||||
[unknownError]="unknownError"
|
||||
[customError]="errorMessage"
|
||||
[formFields]="formFields"
|
||||
(submit)="submit($event)">
|
||||
</cre-entity-add>
|
|
@ -0,0 +1,98 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {RecipeService} from "../../services/recipe.service";
|
||||
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
|
||||
import {FormBuilder, Validators} from "@angular/forms";
|
||||
import {CompanyService} from "../../../company/service/company.service";
|
||||
import {map} from "rxjs/operators";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-add',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.sass']
|
||||
})
|
||||
export class AddComponent extends SubscribingComponent {
|
||||
formFields: FormField[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Nom',
|
||||
icon: 'form-textbox',
|
||||
type: 'text',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
icon: 'text',
|
||||
type: 'text',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Une description est requise'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'sample',
|
||||
label: 'Échantillon',
|
||||
icon: 'pound',
|
||||
type: 'number',
|
||||
validator: Validators.compose([Validators.required, Validators.min(0)]),
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Un numéro d\'échantillon est requis'},
|
||||
{conditionFn: errors => errors.min, message: 'Le numéro d\'échantillon doit être supérieur ou égal à 0'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'approbationDate',
|
||||
label: 'Date d\'approbation',
|
||||
icon: 'calendar',
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
name: 'remark',
|
||||
label: 'Remarque',
|
||||
icon: 'text',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'company',
|
||||
label: 'Bannière',
|
||||
icon: 'domain',
|
||||
type: 'select',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Une bannière est requise'}
|
||||
],
|
||||
options$: this.companyService.all.pipe(map(companies => companies.map(c => {
|
||||
return {value: c.id, label: c.name}
|
||||
})))
|
||||
}
|
||||
]
|
||||
unknownError = false
|
||||
errorMessage: string | null
|
||||
|
||||
constructor(
|
||||
private recipeService: RecipeService,
|
||||
private companyService: CompanyService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
submit(values) {
|
||||
this.subscribe(
|
||||
this.recipeService.save(values.name, values.description, values.sample, values.approbationDate, values.remark, values.company),
|
||||
{
|
||||
next: recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<div *ngIf="recipe">
|
||||
<div *ngIf="!recipe.mixes" class="alert alert-warning m-3">
|
||||
<p>Il n'y a aucun mélange dans cette recette</p>
|
||||
</div>
|
||||
<div *ngIf="recipe.steps.length <= 0" class="alert alert-warning m-3">
|
||||
<p>Il n'y a aucune étape dans cette recette</p>
|
||||
</div>
|
||||
|
||||
<div class="action-bar backward">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="mt-1 pb-2">
|
||||
<button mat-raised-button color="primary" routerLink="/color/list">Retour</button>
|
||||
<button mat-raised-button color="accent" (click)="submit(editComponent)">Enregistrer</button>
|
||||
<button mat-raised-button color="warn" *ngIf="hasDeletePermission" (click)="delete()">Supprimer</button>
|
||||
</div>
|
||||
<mat-form-field>
|
||||
<mat-label>Unités</mat-label>
|
||||
<mat-select [value]="unitConstants.UNIT_MILLILITER" (selectionChange)="changeUnits($event.value)">
|
||||
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
|
||||
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
|
||||
<mat-option [value]="unitConstants.UNIT_GALLON">Gallons</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
</div>
|
||||
|
||||
<div class="recipe-wrapper d-flex flex-row justify-content-around align-items-start flex-wrap">
|
||||
<div>
|
||||
<cre-entity-edit
|
||||
#editComponent
|
||||
title="Modifier la couleur {{recipe.name}}"
|
||||
deleteConfirmMessage="Voulez-vous vraiment supprimer la couleur {{recipe.name}}?"
|
||||
[entity]="recipe"
|
||||
[formFields]="formFields"
|
||||
[unknownError]="unknownError"
|
||||
[customError]="errorMessage"
|
||||
[disableButtons]="true"
|
||||
[noTopMargin]="true">
|
||||
</cre-entity-edit>
|
||||
</div>
|
||||
|
||||
<div class="recipe-mixes-wrapper">
|
||||
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<cre-step-table [steps]="recipe.steps"></cre-step-table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<cre-images-editor #imagesEditor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,2 @@
|
|||
.recipe-wrapper > div
|
||||
margin: 0 3rem 3rem
|
|
@ -0,0 +1,145 @@
|
|||
import {Component, ViewChild} from '@angular/core';
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {Recipe} from "../../../shared/model/recipe.model";
|
||||
import {RecipeService} from "../../services/recipe.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Validators} from "@angular/forms";
|
||||
import {Subject} from "rxjs";
|
||||
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {EmployeePermission} from "../../../shared/model/employee";
|
||||
import {EntityEditComponent} from "../../../shared/components/entity-edit/entity-edit.component";
|
||||
import {ImagesEditorComponent} from "../../components/images-editor/images-editor.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-edit',
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrls: ['./edit.component.sass']
|
||||
})
|
||||
export class EditComponent extends SubscribingComponent {
|
||||
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
|
||||
|
||||
@ViewChild('imagesEditor') imagesEditor: ImagesEditorComponent
|
||||
|
||||
recipe: Recipe | null
|
||||
formFields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Nom',
|
||||
icon: 'form-textbox',
|
||||
type: 'text',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
icon: 'text',
|
||||
type: 'text',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Une description est requise'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'sample',
|
||||
label: 'Échantillon',
|
||||
icon: 'pound',
|
||||
type: 'number',
|
||||
validator: Validators.compose([Validators.required, Validators.min(0)]),
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Un numéro d\'échantillon est requis'},
|
||||
{conditionFn: errors => errors.min, message: 'Le numéro d\'échantillon doit être supérieur ou égal à 0'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'approbationDate',
|
||||
label: 'Date d\'approbation',
|
||||
icon: 'calendar',
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
name: 'remark',
|
||||
label: 'Remarque',
|
||||
icon: 'text',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'company',
|
||||
label: 'Bannière',
|
||||
icon: 'domain',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
valueFn: recipe => recipe.company.name,
|
||||
}
|
||||
]
|
||||
unknownError = false
|
||||
errorMessage: string | null
|
||||
units$ = new Subject<string>()
|
||||
|
||||
constructor(
|
||||
private recipeService: RecipeService,
|
||||
private accountService: AccountService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
||||
this.subscribe(
|
||||
this.recipeService.getById(id),
|
||||
{
|
||||
next: recipe => this.recipe = recipe,
|
||||
error: err => {
|
||||
if (err.status === 404) {
|
||||
this.router.navigate(['/color/list'])
|
||||
} else {
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
changeUnits(unit: string) {
|
||||
this.units$.next(unit)
|
||||
}
|
||||
|
||||
submit(editComponent: EntityEditComponent) {
|
||||
const values = editComponent.values
|
||||
this.subscribe(
|
||||
this.recipeService.update(this.recipe.id, values.name, values.description, values.sample, values.approbationDate, values.remark, this.recipe.steps),
|
||||
{
|
||||
next: () => this.router.navigate(['/color/list']),
|
||||
error: err => {
|
||||
if (err.status === 409) {
|
||||
this.errorMessage = `Une couleur avec le nom '${values.name}' et la bannière '${this.recipe.company.name}' existe déjà`
|
||||
} else {
|
||||
this.unknownError = true
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribe(
|
||||
this.recipeService.delete(this.recipe.id),
|
||||
{
|
||||
next: () => this.router.navigate(['/color/list'])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
get hasDeletePermission(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.REMOVE_RECIPE)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<div *ngIf="recipe">
|
||||
<cre-recipe-info [recipe]="recipe" [hasModifications]="hasModifications"></cre-recipe-info>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger m-3">
|
||||
<p *ngIf="error === ERROR_UNKNOWN">Une erreur est survenue</p>
|
||||
<p *ngIf="error === ERROR_DEDUCT">Certains produit ne sont pas en quantité suffisante dans l'inventaire</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="printingError != 2 && printingError != 1" class="alert alert-danger m-3">
|
||||
<p *ngIf="printingError === -1">L'extension b-Pac n'est pas installée</p>
|
||||
<p *ngIf="printingError === 98">Il n'y a pas de base dans ce mélange</p>
|
||||
<p *ngIf="printingError === 99">Une erreur est survenue pendant l'impression</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="success" class="alert alert-success m-3">
|
||||
<p *ngIf="success === SUCCESS_SAVE">Les modifications ont été enregistrées</p>
|
||||
<p *ngIf="success === SUCCESS_DEDUCT">Les quantités des produits utilisés ont été déduites de l'inventaire</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="printingError === 1" class="alert alert-success m-3">
|
||||
<p>Impression en cours. Cette opération peut prendre quelques secondes.</p>
|
||||
</div>
|
||||
|
||||
<div class="action-bar backward d-flex flex-row">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="mt-1 pb-2">
|
||||
<button mat-raised-button color="primary" routerLink="/color/list">Retour</button>
|
||||
<button mat-raised-button color="primary">Version Excel</button>
|
||||
<button mat-raised-button color="accent" (click)="saveModifications()" [disabled]="!hasModifications">
|
||||
Enregistrer
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="deductQuantities()">Déduire</button>
|
||||
</div>
|
||||
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1"></div>
|
||||
|
||||
<mat-form-field class="w-auto">
|
||||
<mat-label>Note</mat-label>
|
||||
<textarea matInput cols="40" rows="3" (change)="changeNote($event)">{{note}}</textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="recipe-content d-flex flex-row justify-content-around align-items-start flex-wrap mt-5">
|
||||
<!-- Mixes -->
|
||||
<div>
|
||||
<cre-mixes-card
|
||||
[recipe]="recipe"
|
||||
[deductErrorBody]="deductErrorBody"
|
||||
[units$]="units$"
|
||||
(quantityChange)="changeQuantity($event)"
|
||||
(locationChange)="changeMixLocation($event)"
|
||||
(deduct)="deductMixQuantities($event)"
|
||||
[(printingError)]="printingError">
|
||||
</cre-mixes-card>
|
||||
</div>
|
||||
|
||||
<!-- Steps -->
|
||||
<div>
|
||||
<cre-step-list [steps]="recipe.steps"></cre-step-list>
|
||||
</div>
|
||||
|
||||
<!-- Images -->
|
||||
<div>
|
||||
<cre-images-editor [recipe]="recipe" [editionMode]="false"></cre-images-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,2 @@
|
|||
.recipe-content > div
|
||||
margin: 0 3rem 3rem
|
|
@ -0,0 +1,129 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {RecipeService} from "../../services/recipe.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {Recipe} from "../../../shared/model/recipe.model";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-explore',
|
||||
templateUrl: './explore.component.html',
|
||||
styleUrls: ['./explore.component.sass']
|
||||
})
|
||||
export class ExploreComponent extends SubscribingComponent {
|
||||
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
|
||||
|
||||
recipe: Recipe | null
|
||||
error: string
|
||||
deductErrorBody = {}
|
||||
success: string
|
||||
units$ = new Subject<string>()
|
||||
|
||||
hasModifications = false
|
||||
note: string | null
|
||||
quantitiesChanges = new Map<number, Map<number, number>>()
|
||||
mixesLocationChanges = new Map<number, string>()
|
||||
printingError = 2
|
||||
|
||||
// Errors
|
||||
readonly ERROR_UNKNOWN = 'unknown'
|
||||
readonly ERROR_DEDUCT = 'deduct'
|
||||
// Success
|
||||
readonly SUCCESS_SAVE = 'save'
|
||||
readonly SUCCESS_DEDUCT = 'deduct'
|
||||
|
||||
constructor(
|
||||
private recipeService: RecipeService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
|
||||
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
||||
this.subscribe(
|
||||
this.recipeService.getById(id),
|
||||
{
|
||||
next: r => {
|
||||
this.recipe = r
|
||||
this.note = r.note
|
||||
},
|
||||
error: err => this.handleNotFoundError(err, '/colors/list')
|
||||
},
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
changeUnits(unit: string) {
|
||||
this.units$.next(unit)
|
||||
}
|
||||
|
||||
changeNote(event: any) {
|
||||
this.hasModifications = true
|
||||
this.note = event.target.value
|
||||
}
|
||||
|
||||
changeQuantity(event: { id: number, materialId: number, quantity: number }) {
|
||||
if (!this.quantitiesChanges.has(event.id)) this.quantitiesChanges.set(event.id, new Map<number, number>())
|
||||
this.quantitiesChanges.get(event.id).set(event.materialId, event.quantity)
|
||||
}
|
||||
|
||||
changeMixLocation(event: { id: number, location: string }) {
|
||||
this.hasModifications = true
|
||||
this.mixesLocationChanges.set(event.id, event.location)
|
||||
}
|
||||
|
||||
saveModifications() {
|
||||
this.subscribe(
|
||||
this.recipeService.saveExplorerModifications(this.recipe.id, this.note, this.mixesLocationChanges),
|
||||
{
|
||||
next: () => {
|
||||
this.hasModifications = false
|
||||
this.error = null
|
||||
this.success = this.SUCCESS_SAVE
|
||||
},
|
||||
error: err => {
|
||||
this.success = null
|
||||
this.error = this.ERROR_UNKNOWN
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
deductQuantities() {
|
||||
this.performDeductQuantities(this.recipeService.deductQuantities(this.recipe, this.quantitiesChanges))
|
||||
}
|
||||
|
||||
deductMixQuantities(mixId: number) {
|
||||
this.performDeductQuantities(this.recipeService.deductMixQuantities(this.recipe, mixId, this.quantitiesChanges.get(mixId)))
|
||||
}
|
||||
|
||||
performDeductQuantities(observable: Observable<void>) {
|
||||
this.subscribe(
|
||||
observable,
|
||||
{
|
||||
next: () => {
|
||||
this.error = null
|
||||
this.success = this.SUCCESS_DEDUCT
|
||||
},
|
||||
error: err => {
|
||||
this.success = null
|
||||
if (err.status === 409) { // There is not enough of one or more materials in the inventory
|
||||
this.error = this.ERROR_DEDUCT
|
||||
this.deductErrorBody = err.error
|
||||
} else {
|
||||
this.error = this.ERROR_UNKNOWN
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<div class="action-bar">
|
||||
<mat-form-field>
|
||||
<mat-label>Recherche</mat-label>
|
||||
<input matInput type="text" [(ngModel)]="searchQuery"/>
|
||||
<button mat-button *ngIf="searchQuery" matSuffix mat-icon-button (click)="searchQuery=''">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<div class="button-add">
|
||||
<button *ngIf="hasEditPermission" mat-raised-button color="accent" routerLink="/color/add">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-expansion-panel class="table-title" *ngFor="let companyRecipes of (recipes$ | async)"
|
||||
[hidden]="isCompanyHidden(companyRecipes.recipes)" [expanded]="panelForcedExpanded">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
{{companyRecipes.company}}
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<ng-container *ngTemplateOutlet="recipeTableTemplate; context: {recipes: companyRecipes.recipes}"></ng-container>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<ng-template
|
||||
#recipeTableTemplate
|
||||
let-recipes="recipes">
|
||||
<table class="mx-auto" mat-table [dataSource]="recipes">
|
||||
<!-- Recipe's info -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||
<td mat-cell *matCellDef="let recipe">{{recipe.name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let recipe">{{recipe.description}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="sample">
|
||||
<th mat-header-cell *matHeaderCellDef>Échantillon</th>
|
||||
<td mat-cell *matCellDef="let recipe">#{{recipe.sample}}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Icons -->
|
||||
<ng-container matColumnDef="iconNotApproved">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let recipe" [class.disabled]="recipe.approbationDate">
|
||||
<mat-icon svgIcon="alert" class="color-warning" title="Cette recette n'est pas approuvée"></mat-icon>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- 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>
|
||||
</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>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: tableCols" [hidden]="!searchRecipe(row)"></tr>
|
||||
</table>
|
||||
</ng-template>
|
|
@ -0,0 +1,6 @@
|
|||
mat-expansion-panel
|
||||
width: 60rem
|
||||
margin: 20px auto
|
||||
|
||||
.button-add
|
||||
margin-top: .8rem
|
|
@ -0,0 +1,56 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {RecipeService} from "../../services/recipe.service";
|
||||
import {EmployeePermission} from "../../../shared/model/employee";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {Recipe} from "../../../shared/model/recipe.model";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-list',
|
||||
templateUrl: './list.component.html',
|
||||
styleUrls: ['./list.component.sass']
|
||||
})
|
||||
export class ListComponent extends SubscribingComponent {
|
||||
recipes$ = this.recipeService.allSortedByCompany
|
||||
tableCols = ['name', 'description', 'sample', 'iconNotApproved', 'buttonView', 'buttonEdit']
|
||||
searchQuery = ""
|
||||
panelForcedExpanded = false
|
||||
recipesHidden = []
|
||||
|
||||
constructor(
|
||||
private recipeService: RecipeService,
|
||||
private accountService: AccountService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
searchRecipe(recipe: Recipe) {
|
||||
if (this.searchQuery.length > 0) {
|
||||
this.panelForcedExpanded = true
|
||||
}
|
||||
const positive = this.searchString(recipe.name) ||
|
||||
this.searchString(recipe.description) ||
|
||||
this.searchString(recipe.sample.toString())
|
||||
this.recipesHidden[recipe.id] = !positive
|
||||
return positive
|
||||
}
|
||||
|
||||
isCompanyHidden(companyRecipes: Recipe[]): boolean {
|
||||
return (this.searchQuery && this.searchQuery.length > 0) && companyRecipes.map(r => this.recipesHidden[r.id]).filter(r => !r).length <= 0
|
||||
}
|
||||
|
||||
get hasEditPermission(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_RECIPE)
|
||||
}
|
||||
|
||||
private searchString(value: string): boolean {
|
||||
return value.toLowerCase().indexOf(this.searchQuery.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<cre-mix-editor
|
||||
[recipeId]="recipeId"
|
||||
[materials]="materials"
|
||||
(save)="submit($event)">
|
||||
</cre-mix-editor>
|
|
@ -0,0 +1,43 @@
|
|||
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 {SubscribingComponent} from "../../../../shared/components/subscribing.component";
|
||||
import {MixService} from "../../../services/mix.service";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-mix-add',
|
||||
templateUrl: './mix-add.component.html',
|
||||
styleUrls: ['./mix-add.component.sass']
|
||||
})
|
||||
export class MixAddComponent extends SubscribingComponent {
|
||||
recipeId: number | null
|
||||
materials: Material[] | null
|
||||
|
||||
constructor(
|
||||
private materialService: MaterialService,
|
||||
private mixService: MixService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
|
||||
this.recipeId = this.urlUtils.parseIntUrlParam('recipeId')
|
||||
|
||||
this.subscribe(
|
||||
this.materialService.getAllForMixCreation(this.recipeId),
|
||||
{next: m => this.materials = m}
|
||||
)
|
||||
}
|
||||
|
||||
submit(values) {
|
||||
this.subscribe(
|
||||
this.mixService.saveWithUnits(values.name, values.recipeId, values.materialTypeId, values.mixMaterials, values.units),
|
||||
{next: () => this.urlUtils.navigateTo(`/color/edit/${this.recipeId}`)}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<cre-mix-editor
|
||||
[mixId]="mixId"
|
||||
[recipeId]="recipeId"
|
||||
[materials]="materials"
|
||||
(save)="submit($event)">
|
||||
</cre-mix-editor>
|
|
@ -0,0 +1,45 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {SubscribingComponent} 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";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-mix-edit',
|
||||
templateUrl: './mix-edit.component.html',
|
||||
styleUrls: ['./mix-edit.component.sass']
|
||||
})
|
||||
export class MixEditComponent extends SubscribingComponent {
|
||||
mixId: number | null
|
||||
recipeId: number | null
|
||||
materials: Material[] | null
|
||||
|
||||
constructor(
|
||||
private materialService: MaterialService,
|
||||
private mixService: MixService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(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),
|
||||
{next: m => this.materials = m}
|
||||
)
|
||||
}
|
||||
|
||||
submit(values) {
|
||||
this.subscribe(
|
||||
this.mixService.updateWithUnits(this.mixId, values.name, values.materialTypeId, values.mixMaterials, values.units),
|
||||
{next: () => this.urlUtils.navigateTo(`/color/edit/${this.recipeId}`)}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import * as bpac from "./bpac.js";
|
||||
|
||||
export class PtouchPrinter {
|
||||
constructor(object) {
|
||||
this.object = object;
|
||||
this.pdocument = bpac.IDocument;
|
||||
}
|
||||
|
||||
async print() {
|
||||
if (!this.isBPacExtensionInstalled()) {
|
||||
console.error("L'extension b-Pac n'est pas installée");
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.openDoc();
|
||||
await this.fillDoc();
|
||||
this.printDoc();
|
||||
this.pdocument.Close();
|
||||
|
||||
return 1;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 99;
|
||||
}
|
||||
};
|
||||
|
||||
async openDoc() {
|
||||
const docUrl = `${baseUrl}/lbx/${this.object.template}.lbx`;
|
||||
console.log("Ouverture du modèle: " + docUrl);
|
||||
await this.pdocument.Open(docUrl);
|
||||
}
|
||||
|
||||
async fillDoc() {
|
||||
for (let i = 0; i < this.object.lines.length; i++) {
|
||||
const line = this.object.lines[i];
|
||||
const label = await this.pdocument.GetObject(line.name);
|
||||
label.Text = line.value;
|
||||
}
|
||||
}
|
||||
|
||||
printDoc() {
|
||||
this.pdocument.StartPrint("", 0);
|
||||
this.pdocument.PrintOut(1, 0);
|
||||
this.pdocument.EndPrint();
|
||||
}
|
||||
|
||||
isBPacExtensionInstalled() {
|
||||
return bpac.IsExtensionInstalled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {convertMixMaterialQuantity, UNIT_MILLILITER} from "../../shared/units";
|
||||
import {Observable} from "rxjs";
|
||||
import {Mix} from "../../shared/model/recipe.model";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MixService {
|
||||
constructor(
|
||||
private api: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
get all(): Observable<Mix[]> {
|
||||
return this.api.get<Mix[]>('/recipe/mix')
|
||||
}
|
||||
|
||||
getById(id: number): Observable<Mix> {
|
||||
return this.api.get<Mix>(`/recipe/mix/${id}`)
|
||||
}
|
||||
|
||||
saveWithUnits(name: string, recipeId: number, materialTypeId: number, mixMaterials: { materialId: number, quantity: number, percents: boolean }[], units: string): Observable<void> {
|
||||
return this.save(name, recipeId, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
|
||||
}
|
||||
|
||||
save(name: string, recipeId: number, materialTypeId: number, mixMaterials: { materialId: number, quantity: number }[]): Observable<void> {
|
||||
const body = {
|
||||
name,
|
||||
recipeId,
|
||||
materialTypeId,
|
||||
mixMaterials: {}
|
||||
}
|
||||
this.appendMixMaterialsToBody(mixMaterials, body)
|
||||
return this.api.post('/recipe/mix', body)
|
||||
}
|
||||
|
||||
updateWithUnits(id: number, name: string, materialTypeId: number, mixMaterials: { materialId: number, quantity: number, percents: boolean }[], units: string): Observable<void> {
|
||||
return this.update(id, name, materialTypeId, this.convertMixMaterialsToMl(mixMaterials, units))
|
||||
}
|
||||
|
||||
update(id: number, name: string, materialTypeId: number, mixMaterials: { materialId: number, quantity: number }[]): Observable<void> {
|
||||
const body = {
|
||||
id,
|
||||
name,
|
||||
materialTypeId,
|
||||
mixMaterials: {}
|
||||
}
|
||||
this.appendMixMaterialsToBody(mixMaterials, body)
|
||||
return this.api.put('/recipe/mix', body)
|
||||
}
|
||||
|
||||
delete(id: number): Observable<void> {
|
||||
return this.api.delete(`/recipe/mix/${id}`)
|
||||
}
|
||||
|
||||
extractMixMaterials(mix: Mix): { materialId: number, quantity: number, percents: boolean }[] {
|
||||
return mix.mixMaterials.map(m => {
|
||||
return {materialId: m.material.id, quantity: m.quantity, percents: m.material.materialType.usePercentages}
|
||||
})
|
||||
}
|
||||
|
||||
private convertMixMaterialsToMl(mixMaterials: { materialId: number, quantity: number, percents: boolean }[], units: string): { materialId: number, quantity: number }[] {
|
||||
return mixMaterials.map(m => {
|
||||
return {
|
||||
materialId: m.materialId,
|
||||
quantity: convertMixMaterialQuantity(m, units, UNIT_MILLILITER)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private appendMixMaterialsToBody(mixMaterials: { materialId: number, quantity: number }[], body: any) {
|
||||
mixMaterials
|
||||
.filter(m => m.materialId != null && m.quantity != null)
|
||||
.forEach(m => body.mixMaterials[m.materialId] = m.quantity)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RecipeImageService {
|
||||
constructor(
|
||||
private api: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
getAllIdsForRecipe(recipeId: number): Observable<number[]> {
|
||||
return this.api.get(`/recipe/${recipeId}/image`)
|
||||
}
|
||||
|
||||
save(image: File, recipeId: number): Observable<void> {
|
||||
const body = new FormData()
|
||||
body.append('image', image)
|
||||
return this.api.post<void>(`/recipe/${recipeId}/image`, body, true)
|
||||
}
|
||||
|
||||
deleteAll(imageIds: number[], recipeId: number) {
|
||||
imageIds.forEach(id => this.delete(id, recipeId))
|
||||
}
|
||||
|
||||
delete(imageId: number, recipeId: number): Observable<void> {
|
||||
return this.api.delete<void>(`/recipe/${recipeId}/image/${imageId}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {Recipe, RecipeStep} from "../../shared/model/recipe.model";
|
||||
import {map} from "rxjs/operators";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RecipeService {
|
||||
constructor(
|
||||
private api: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
get all(): Observable<Recipe[]> {
|
||||
return this.api.get<Recipe[]>('/recipe')
|
||||
}
|
||||
|
||||
get allSortedByCompany(): Observable<{ company: string, recipes: Recipe[] }[]> {
|
||||
return this.all.pipe(map(recipes => {
|
||||
const mapped = []
|
||||
recipes.forEach(r => {
|
||||
if (!mapped[r.company.id]) {
|
||||
mapped[r.company.id] = {company: r.company.name, recipes: []}
|
||||
}
|
||||
mapped[r.company.id].recipes.push(r)
|
||||
})
|
||||
return mapped.filter(e => e != null) // Filter to remove empty elements in the array that appears for some reason
|
||||
}))
|
||||
}
|
||||
|
||||
getById(id: number): Observable<Recipe> {
|
||||
return this.api.get<Recipe>(`/recipe/${id}`)
|
||||
}
|
||||
|
||||
save(name: string, description: string, sample: number, approbationDate: string, remark: string, companyId: number): Observable<Recipe> {
|
||||
const body = {name, description, sample, remark, companyId}
|
||||
if (approbationDate) {
|
||||
// @ts-ignore
|
||||
body.approbationDate = approbationDate
|
||||
}
|
||||
return this.api.post<Recipe>('/recipe', body)
|
||||
}
|
||||
|
||||
update(id: number, name: string, description: string, sample: number, approbationDate: string, remark: string, steps: RecipeStep[] = null) {
|
||||
const body = {id, name, description, sample, remark, steps}
|
||||
if (approbationDate) {
|
||||
// @ts-ignore
|
||||
body.approbationDate = approbationDate
|
||||
}
|
||||
return this.api.put<Recipe>('/recipe', body)
|
||||
}
|
||||
|
||||
saveExplorerModifications(id: number, note: string, mixesLocationChange: Map<number, string>): Observable<void> {
|
||||
const body = {
|
||||
id,
|
||||
note,
|
||||
mixesLocation: {}
|
||||
}
|
||||
mixesLocationChange.forEach((l, i) => body.mixesLocation[i] = l)
|
||||
|
||||
return this.api.put<void>('/recipe/public', body)
|
||||
}
|
||||
|
||||
deductMixQuantities(recipe: Recipe, mixId: number, quantities: Map<number, number>): Observable<void> {
|
||||
return this.sendDeductBody(this.buildDeductMixBody(recipe, mixId, quantities))
|
||||
}
|
||||
|
||||
deductQuantities(recipe: Recipe, quantities: Map<number, Map<number, number>>): Observable<void> {
|
||||
return this.sendDeductBody(this.buildDeductBody(recipe, quantities))
|
||||
}
|
||||
|
||||
delete(id: number): Observable<void> {
|
||||
return this.api.delete<void>(`/recipe/${id}`)
|
||||
}
|
||||
|
||||
private buildDeductMixBody(recipe: Recipe, mixId: number, quantities: Map<number, number>): any {
|
||||
const mix = recipe.mixes.filter(m => m.id === mixId)[0]
|
||||
const body = {id: recipe.id, quantities: {}}
|
||||
body.quantities[mixId] = {}
|
||||
const firstMaterial = mix.mixMaterials[0].material.id
|
||||
mix.mixMaterials.forEach(m => {
|
||||
if (quantities && quantities.has(m.material.id)) {
|
||||
body.quantities[mix.id][m.material.id] = quantities.get(m.material.id)
|
||||
} else {
|
||||
let quantity = m.quantity
|
||||
if (m.material.materialType.usePercentages) quantity = body.quantities[mix.id][firstMaterial] * (quantity / 100)
|
||||
body.quantities[mix.id][m.material.id] = quantity
|
||||
}
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
private buildDeductBody(recipe: Recipe, quantities: Map<number, Map<number, number>>): any {
|
||||
const body = {id: recipe.id, quantities: {}}
|
||||
recipe.mixes.forEach(mix => {
|
||||
body.quantities[mix.id] = this.buildDeductMixBody(recipe, mix.id, quantities.get(mix.id)).quantities[mix.id]
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
private sendDeductBody(body: any): Observable<void> {
|
||||
return this.api.put('/recipe/deduct', body)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import {RouterModule, Routes} from "@angular/router";
|
||||
import {ListComponent} from "./pages/list/list.component";
|
||||
import {NgModule} from "@angular/core";
|
||||
import {AddComponent} from "./pages/add/add.component";
|
||||
import {EditComponent} from "./pages/edit/edit.component";
|
||||
|
||||
const routes: Routes = [{
|
||||
path: 'list',
|
||||
component: ListComponent
|
||||
}, {
|
||||
path: 'add',
|
||||
component: AddComponent
|
||||
}, {
|
||||
path: 'edit/:id',
|
||||
component: EditComponent
|
||||
}, {
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'list'
|
||||
}]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class CompanyRoutingModule {
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ListComponent } from './pages/list/list.component';
|
||||
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";
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ListComponent, AddComponent, EditComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
CompanyRoutingModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class CompanyModule { }
|
|
@ -0,0 +1,8 @@
|
|||
<cre-entity-add
|
||||
title="Création d'une bannière"
|
||||
backButtonLink="/catalog/company/list"
|
||||
[unknownError]="unknownError"
|
||||
[customError]="errorMessage"
|
||||
[formFields]="formFields"
|
||||
(submit)="submit($event)">
|
||||
</cre-entity-add>
|
|
@ -0,0 +1,53 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {CompanyService} from "../../service/company.service";
|
||||
import {FormBuilder, Validators} from "@angular/forms";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-add',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.sass']
|
||||
})
|
||||
export class AddComponent extends SubscribingComponent {
|
||||
formFields: FormField[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Nom',
|
||||
icon: 'form-textbox',
|
||||
type: 'text',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
|
||||
]
|
||||
}
|
||||
]
|
||||
unknownError = false
|
||||
errorMessage: string | null
|
||||
|
||||
constructor(
|
||||
private companyService: CompanyService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
submit(values) {
|
||||
this.subscribe(
|
||||
this.companyService.save(values.name),
|
||||
{
|
||||
next: () => this.router.navigate(['/catalog/company/list']),
|
||||
error: err => {
|
||||
if (err.status === 409) {
|
||||
this.errorMessage = `Une bannière avec le nom '${values.name}' existe déjà`
|
||||
} else {
|
||||
this.unknownError = true
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<cre-entity-edit
|
||||
*ngIf="company"
|
||||
title="Modifier la bannière {{company.name}}"
|
||||
deleteConfirmMessage="Voulez-vous vraiment supprimer la bannière {{company.name}}?"
|
||||
backButtonLink="/catalog/company/list"
|
||||
deletePermission="REMOVE_COMPANY"
|
||||
[entity]="company"
|
||||
[formFields]="formFields"
|
||||
[unknownError]="unknownError"
|
||||
[customError]="errorMessage"
|
||||
(submit)="submit($event)"
|
||||
(delete)="delete()">
|
||||
</cre-entity-edit>
|
|
@ -0,0 +1,88 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {Company} from "../../../shared/model/company.model";
|
||||
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
|
||||
import {FormBuilder, Validators} from "@angular/forms";
|
||||
import {CompanyService} from "../../service/company.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-edit',
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrls: ['./edit.component.sass']
|
||||
})
|
||||
export class EditComponent extends SubscribingComponent {
|
||||
company: Company | null
|
||||
formFields: FormField[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Nom',
|
||||
icon: 'form-textbox',
|
||||
type: 'text',
|
||||
validator: Validators.required,
|
||||
errorMessages: [
|
||||
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
|
||||
]
|
||||
}
|
||||
]
|
||||
unknownError = false
|
||||
errorMessage: string | null
|
||||
|
||||
constructor(
|
||||
private companyService: CompanyService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
|
||||
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
|
||||
this.subscribe(
|
||||
this.companyService.getById(id),
|
||||
{
|
||||
next: company => this.company = company,
|
||||
error: err => {
|
||||
if (err.status == 404) {
|
||||
this.router.navigate(['/catalog/company/list'])
|
||||
} else {
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
submit(values) {
|
||||
this.subscribe(
|
||||
this.companyService.update(this.company.id, values.name),
|
||||
{
|
||||
next: () => this.router.navigate(['/catalog/company/list']),
|
||||
error: err => {
|
||||
if (err.status == 409) {
|
||||
this.errorMessage = `Une bannière avec le nom '${values.name}' existe déjà`
|
||||
} else {
|
||||
this.unknownError = true
|
||||
}
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribe(
|
||||
this.companyService.delete(this.company.id),
|
||||
{
|
||||
next: () => this.router.navigate(['/catalog/company/list']),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<cre-entity-list
|
||||
[entities$]="companies$"
|
||||
[columns]="columns"
|
||||
[buttons]="buttons"
|
||||
addLink="/catalog/company/add"
|
||||
addPermission="EDIT_COMPANY">
|
||||
</cre-entity-list>
|
|
@ -0,0 +1,31 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {CompanyService} from "../../service/company.service";
|
||||
import {EmployeePermission} from "../../../shared/model/employee";
|
||||
import {FormBuilder} from "@angular/forms";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-list',
|
||||
templateUrl: './list.component.html',
|
||||
styleUrls: ['./list.component.sass']
|
||||
})
|
||||
export class ListComponent extends SubscribingComponent {
|
||||
companies$ = this.companyService.all
|
||||
columns = [
|
||||
{def: 'name', title: 'Nom', valueFn: c => c.name}
|
||||
]
|
||||
buttons = [{
|
||||
text: 'Modifier',
|
||||
linkFn: t => `/catalog/company/edit/${t.id}`,
|
||||
permission: EmployeePermission.EDIT_COMPANY
|
||||
}]
|
||||
|
||||
constructor(
|
||||
private companyService: CompanyService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from "../../shared/service/api.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {Company} from "../../shared/model/company.model";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CompanyService {
|
||||
constructor(
|
||||
private api: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
get all(): Observable<Company[]> {
|
||||
return this.api.get<Company[]>('/company')
|
||||
}
|
||||
|
||||
getById(id: number): Observable<Company> {
|
||||
return this.api.get<Company>(`/company/${id}`)
|
||||
}
|
||||
|
||||
save(name: string): Observable<void> {
|
||||
return this.api.post<void>('/company', {name})
|
||||
}
|
||||
|
||||
update(id: number, name: string): Observable<void> {
|
||||
return this.api.put<void>('/company', {id, name})
|
||||
}
|
||||
|
||||
delete(id: number): Observable<void> {
|
||||
return this.api.delete<void>(`/company/${id}`)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ListComponent } from './pages/list/list.component';
|
||||
import {AddComponent} from "./pages/add/add.component";
|
||||
import {EditComponent} from "./pages/edit/edit.component";
|
||||
import {PasswordEditComponent} from "./pages/password-edit/password-edit.component";
|
||||
|
||||
const routes: Routes = [{ path: 'list', component: ListComponent }, {path: 'add', component: AddComponent}, {path: 'edit/:id', component: EditComponent}, {path: 'password/edit/:id', component: PasswordEditComponent}, {path: '', redirectTo: 'list'}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class EmployeesRoutingModule { }
|
|
@ -0,0 +1,20 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {EmployeesRoutingModule} from './employees-routing.module';
|
||||
import {ListComponent} from './pages/list/list.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import { AddComponent } from './pages/add/add.component';
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import { EditComponent } from './pages/edit/edit.component';
|
||||
import { PasswordEditComponent } from './pages/password-edit/password-edit.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ListComponent, AddComponent, EditComponent, PasswordEditComponent],
|
||||
imports: [
|
||||
EmployeesRoutingModule,
|
||||
SharedModule,
|
||||
MatSelectModule
|
||||
]
|
||||
})
|
||||
export class EmployeesModule { }
|
|
@ -0,0 +1,60 @@
|
|||
<mat-card class="x-centered mt-5">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Création d'un employé</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="unknownError" class="alert alert-danger">
|
||||
<p>Une erreur est survenue</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'employé</mat-label>
|
||||
<input matInput type="text" [formControl]="idControl"/>
|
||||
<mat-icon svgIcon="pound" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="idControl.invalid">
|
||||
<span *ngIf="idControl.errors.required">Un numéro d'employé est requis</span>
|
||||
<span *ngIf="idControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Prénom</mat-label>
|
||||
<input matInput type="text" [formControl]="firstNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="firstNameControl.invalid">
|
||||
<span *ngIf="firstNameControl.errors.required">Un prénom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="lastNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="lastNameControl.invalid">
|
||||
<span *ngIf="lastNameControl.errors.required">Un nom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input matInput type="password" [formControl]="passwordControl"/>
|
||||
<mat-icon svgIcon="lock" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="passwordControl.invalid">
|
||||
<span *ngIf="passwordControl.errors.required">Un mot de passe est requis</span>
|
||||
<span *ngIf="passwordControl.errors.minlength">Le mot de passe doit comprendre au moins 8 caractères</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="group$ | async as groups">
|
||||
<mat-label>Groupe</mat-label>
|
||||
<mat-select [formControl]="groupControl">
|
||||
<mat-option [value]="null">Aucun</mat-option>
|
||||
<mat-option *ngFor="let group of groups" [value]="group.id">{{group.name}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
|
||||
</mat-form-field>
|
||||
<cre-permissions-field #permissionsField></cre-permissions-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Créer</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,2 @@
|
|||
mat-card
|
||||
max-width: 90rem
|
|
@ -0,0 +1,79 @@
|
|||
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
|
||||
import {EmployeeGroup} from "../../../shared/model/employee";
|
||||
import {Observable, Subject} from "rxjs";
|
||||
import {GroupService} from "../../../groups/services/group.service";
|
||||
import {take, takeUntil} from "rxjs/operators";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-add',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.sass']
|
||||
})
|
||||
export class AddComponent extends SubscribingComponent {
|
||||
@ViewChild('permissionsField', {static: true}) permissionsField: PermissionsFieldComponent
|
||||
|
||||
form: FormGroup
|
||||
idControl: FormControl
|
||||
firstNameControl: FormControl
|
||||
lastNameControl: FormControl
|
||||
passwordControl: FormControl
|
||||
groupControl: FormControl
|
||||
unknownError = false
|
||||
|
||||
group$: Observable<EmployeeGroup[]> | null
|
||||
|
||||
constructor(
|
||||
private employeeService: EmployeeService,
|
||||
private groupService: GroupService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
|
||||
this.group$ = this.groupService.all
|
||||
|
||||
this.idControl = new FormControl(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)]))
|
||||
this.firstNameControl = new FormControl(null, Validators.required)
|
||||
this.lastNameControl = new FormControl(null, Validators.required)
|
||||
this.passwordControl = new FormControl(null, Validators.compose([Validators.required, Validators.minLength(8)]))
|
||||
this.groupControl = new FormControl(null, Validators.min(0))
|
||||
this.form = new FormGroup({
|
||||
id: this.idControl,
|
||||
firstName: this.firstNameControl,
|
||||
lastName: this.lastNameControl,
|
||||
password: this.passwordControl,
|
||||
group: this.groupControl
|
||||
})
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.permissionsField.valid() && this.form.valid) {
|
||||
this.subscribe(
|
||||
this.employeeService.save(
|
||||
parseInt(this.idControl.value),
|
||||
this.firstNameControl.value,
|
||||
this.lastNameControl.value,
|
||||
this.passwordControl.value,
|
||||
this.groupControl.value,
|
||||
this.permissionsField.allEnabledPermissions
|
||||
),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<mat-card *ngIf="employee" class="x-centered mt-5">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Modification de l'employé #{{employee.id}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="unknownError" class="alert alert-danger">
|
||||
<p>Une erreur est survenue</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<mat-label>Numéro d'employé</mat-label>
|
||||
<input matInput type="text" [formControl]="idControl"/>
|
||||
<mat-icon svgIcon="pound" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="idControl.invalid">
|
||||
<span *ngIf="idControl.errors.required">Un numéro d'employé est requis</span>
|
||||
<span *ngIf="idControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Prénom</mat-label>
|
||||
<input matInput type="text" [formControl]="firstNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="firstNameControl.invalid">
|
||||
<span *ngIf="firstNameControl.errors.required">Un prénom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput type="text" [formControl]="lastNameControl"/>
|
||||
<mat-icon svgIcon="account" matSuffix></mat-icon>
|
||||
<mat-error *ngIf="lastNameControl.invalid">
|
||||
<span *ngIf="lastNameControl.errors.required">Un nom est requis</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="group$ | async as groups">
|
||||
<mat-label>Groupe</mat-label>
|
||||
<mat-select [formControl]="groupControl">
|
||||
<mat-option [value]="null">Aucun</mat-option>
|
||||
<mat-option *ngFor="let group of groups" [value]="group.id">{{group.name}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
|
||||
</mat-form-field>
|
||||
<cre-permissions-field #permissionsField [enabledPermissions]="employee.permissions"></cre-permissions-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="warn" *ngIf="canRemoveEmployee" (click)="confirmBoxComponent.show()">Supprimer
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="submit(permissionsField)" [disabled]="form.invalid">Enregistrer</button>
|
||||
</mat-card-actions>
|
||||
<cre-confirm-box #confirmBoxComponent message="Voulez-vous vraiment supprimer l'employé {{employee.id}}?" (confirm)="delete()"></cre-confirm-box>
|
||||
</mat-card>
|
|
@ -0,0 +1,2 @@
|
|||
mat-card
|
||||
max-width: 90rem
|
|
@ -0,0 +1,161 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {PermissionsFieldComponent} from "../../../shared/components/permissions-field/permissions-field.component";
|
||||
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {GroupService} from "../../../groups/services/group.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Observable} from "rxjs";
|
||||
import {Employee, EmployeeGroup, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-edit',
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrls: ['./edit.component.sass']
|
||||
})
|
||||
export class EditComponent extends SubscribingComponent {
|
||||
employee: Employee | null
|
||||
unknownError = false
|
||||
|
||||
group$: Observable<EmployeeGroup[]> | null
|
||||
|
||||
private _idControl: FormControl
|
||||
private _firstNameControl: FormControl
|
||||
private _lastNameControl: FormControl
|
||||
private _groupControl: FormControl
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private employeeService: EmployeeService,
|
||||
private groupService: GroupService,
|
||||
private formBuilder: FormBuilder,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const employeeId = this.activatedRoute.snapshot.paramMap.get("id")
|
||||
this.subscribe(
|
||||
this.employeeService.get(parseInt(employeeId)),
|
||||
{
|
||||
next: employee => this.employee = employee,
|
||||
error: err => {
|
||||
if (err.status === 404) {
|
||||
this.router.navigate(['/employee/list'])
|
||||
} else {
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
this.group$ = this.groupService.all
|
||||
}
|
||||
|
||||
submit(permissionsField: PermissionsFieldComponent) {
|
||||
if (permissionsField.valid() && this.form.valid) {
|
||||
this.subscribe(
|
||||
this.employeeService.update(
|
||||
parseInt(this.idControl.value),
|
||||
this.firstNameControl.value,
|
||||
this.lastNameControl.value,
|
||||
permissionsField.allEnabledPermissions
|
||||
),
|
||||
{
|
||||
next: () => {
|
||||
const group = parseInt(this._groupControl.value)
|
||||
if (!isNaN(group)) {
|
||||
this.subscribe(
|
||||
this.groupService.addEmployeeToGroup(group, this.employee),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (this.employee.group) {
|
||||
this.subscribe(
|
||||
this.groupService.removeEmployeeFromGroup(this.employee),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.router.navigate(['/employee/list'])
|
||||
}
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.error(err)
|
||||
this.unknownError = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.subscribe(
|
||||
this.employeeService.delete(this.employee.id),
|
||||
{
|
||||
next: () => this.router.navigate(['/employee/list']),
|
||||
error: err => {
|
||||
this.unknownError = true
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
get form(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
id: this._idControl,
|
||||
firstName: this._firstNameControl,
|
||||
lastName: this._lastNameControl,
|
||||
group: this._groupControl
|
||||
})
|
||||
}
|
||||
|
||||
get idControl(): FormControl {
|
||||
this._idControl = this.lazyControl(this._idControl, () => new FormControl(this.employee.id, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)])))
|
||||
return this._idControl
|
||||
}
|
||||
|
||||
get firstNameControl(): FormControl {
|
||||
this._firstNameControl = this.lazyControl(this._firstNameControl, () => new FormControl(this.employee.firstName, Validators.required))
|
||||
return this._firstNameControl
|
||||
}
|
||||
|
||||
get lastNameControl(): FormControl {
|
||||
this._lastNameControl = this.lazyControl(this._lastNameControl, () => new FormControl(this.employee.lastName, Validators.required))
|
||||
return this._lastNameControl
|
||||
}
|
||||
|
||||
get groupControl(): FormControl {
|
||||
this._groupControl = this.lazyControl(this._groupControl, () => new FormControl(this.employee.group?.id))
|
||||
return this._groupControl
|
||||
}
|
||||
|
||||
private lazyControl(control: FormControl, provider: () => FormControl): FormControl {
|
||||
if (control) return control
|
||||
if (this.employee) {
|
||||
return provider()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
get canRemoveEmployee(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.REMOVE_EMPLOYEE)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<div class="action-bar">
|
||||
<button *ngIf="canEditEmployee" mat-raised-button color="accent" routerLink="/employee/add">Ajouter</button>
|
||||
</div>
|
||||
|
||||
<table class="mx-auto" *ngIf="employees$ | async as employees" mat-table multiTemplateDataRows [dataSource]="employees">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Numéro d'employé</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.id}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||
<td mat-cell *matCellDef="let employee">{{employee.firstName}} {{employee.lastName}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="group">
|
||||
<th mat-header-cell *matHeaderCellDef>Groupe</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ng-container *ngIf="employee.group">{{employee.group.name}}</ng-container>
|
||||
<ng-container *ngIf="!employee.group">Aucun</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="lastLogin">
|
||||
<th mat-header-cell *matHeaderCellDef>Dernière connexion</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ng-container *ngIf="employee.lastLoginTime">{{getDate(employee.lastLoginTime).toLocaleString()}}</ng-container>
|
||||
<ng-container *ngIf="!employee.lastLoginTime">Jamais</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="permissionCount">
|
||||
<th mat-header-cell *matHeaderCellDef>Permissions</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ng-container *ngIf="employee.permissions">{{employee.permissions.length}}</ng-container>
|
||||
<ng-container *ngIf="!employee.permissions">0</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="editButton">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canEditEmployee" *matCellDef="let employee">
|
||||
<button mat-raised-button color="accent" routerLink="/employee/edit/{{employee.id}}">Modifier</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="editPasswordButton">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell [class.disabled]="!canEditEmployeePassword" *matCellDef="let employee">
|
||||
<button mat-raised-button color="accent" routerLink="/employee/password/edit/{{employee.id}}">Modifier mot de passe</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="expandedDetail">
|
||||
<td mat-cell *matCellDef="let employee" [attr.colspan]="columns.length">
|
||||
<div class="entity-detail"
|
||||
[@detailExpand]="employee == expandedElement ? 'expanded' : 'collapsed'">
|
||||
<cre-permissions-list [employee]="employee" class="w-100"></cre-permissions-list>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let employee; columns: columns"
|
||||
class="entity-row can-expand"
|
||||
[class.expanded-row]="expandedElement === employee"
|
||||
(click)="expandedElement = expandedElement === employee ? null : employee">
|
||||
</tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
|
||||
</table>
|
|
@ -0,0 +1,2 @@
|
|||
th, td
|
||||
padding: 0 .7rem !important
|
|
@ -0,0 +1,54 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Observable} from "rxjs";
|
||||
import {EmployeeService} from "../../services/employee.service";
|
||||
import {Employee, EmployeePermission} from "../../../shared/model/employee";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {AccountService} from "../../../accounts/services/account.service";
|
||||
import {animate, state, style, transition, trigger} from "@angular/animations";
|
||||
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
|
||||
import {FormBuilder} from "@angular/forms";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'cre-employees',
|
||||
templateUrl: './list.component.html',
|
||||
styleUrls: ['./list.component.sass'],
|
||||
animations: [
|
||||
trigger('detailExpand', [
|
||||
state('collapsed', style({height: '0px', minHeight: '0'})),
|
||||
state('expanded', style({height: '*'})),
|
||||
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
|
||||
])
|
||||
]
|
||||
})
|
||||
export class ListComponent extends SubscribingComponent {
|
||||
employees$: Observable<Employee[]>
|
||||
columns = ['id', 'name', 'group', 'permissionCount', 'lastLogin', 'editButton', 'editPasswordButton']
|
||||
|
||||
expandedElement: Employee | null
|
||||
|
||||
constructor(
|
||||
private employeeService: EmployeeService,
|
||||
private accountService: AccountService,
|
||||
router: Router,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super(activatedRoute, router)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.employees$ = this.employeeService.all.pipe(takeUntil(this.destroy$))
|
||||
}
|
||||
|
||||
getDate(dateString: string) {
|
||||
return new Date(dateString)
|
||||
}
|
||||
|
||||
get canEditEmployee(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE)
|
||||
}
|
||||
|
||||
get canEditEmployeePassword(): boolean {
|
||||
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE_PASSWORD)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<mat-card *ngIf="employee" class="x-centered mt-5">
|
||||
<form [formGroup]="form">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Modification du mot de passe de l'employé #{{employee.id}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input type="password" matInput [formControl]="passwordControl"/>
|
||||
<mat-icon matSuffix svgIcon="lock"></mat-icon>
|
||||
<mat-error *ngIf="passwordControl.invalid">
|
||||
<span *ngIf="passwordControl.errors.required">Un mot de passe est requis</span>
|
||||
<span *ngIf="passwordControl.errors.minlength">Le mot de passe doit comprendre au moins 8 caractères</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
|
||||
<button mat-raised-button color="accent" [disabled]="form.invalid" (click)="submit()">Enregistrer</button>
|
||||
</mat-card-actions>
|
||||
</form>
|
||||
</mat-card>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue