Inventaire

This commit is contained in:
FyloZ 2021-03-20 13:47:57 -04:00
parent e693625ab7
commit 83d8b30207
22 changed files with 387 additions and 167 deletions

View File

@ -21,13 +21,16 @@ 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
]
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent, ImagesEditorComponent, MixesCardComponent],
exports: [
UnitSelectorComponent
],
imports: [
ColorsRoutingModule,
SharedModule,
MatExpansionModule,
FormsModule
]
})
export class ColorsModule {
}

View File

@ -15,7 +15,7 @@
<button mat-raised-button color="accent" (click)="printingConfirmBox.show()">Imprimer</button>
</div>
<div>
<button mat-raised-button color="accent" (click)="deduct.emit()" disabled title="WIP">Déduire</button>
<button mat-raised-button color="accent" (click)="deduct.emit()">Déduire</button>
</div>
</ng-container>
<ng-container *ngIf="editionMode">
@ -115,7 +115,7 @@
<tr mat-header-row *matHeaderRowDef="mixColumns"></tr>
<tr mat-row
*matRowDef="let mixMaterial; columns: mixColumns"
[class.low-quantity]="!editionMode && isInLowQuantity(mixMaterial.id)"
[class.low-quantity]="!editionMode && isInLowQuantity(mixMaterial.material.id)"
(mouseover)="hoveredMixMaterial = mixMaterial">
</tr>
<ng-container *ngIf="!editionMode">

View File

@ -119,7 +119,8 @@ export class MixTableComponent extends SubscribingComponent {
}
isInLowQuantity(materialId: number): boolean {
return this.deductErrorBody[this.mix.id] && this.deductErrorBody[this.mix.id].indexOf(materialId) >= 0
return this.deductErrorBody.mix === this.mix.id &&
this.deductErrorBody.lowQuantities.filter(l => l.material === materialId).length > 0
}
round(quantity: number): number {

View File

@ -9,7 +9,6 @@
<button mat-raised-button color="accent" (click)="saveModifications()" [disabled]="!hasModifications">
Enregistrer
</button>
<button mat-raised-button color="accent" (click)="deductQuantities()" disabled title="WIP">Déduire</button>
</div>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
</div>
@ -31,7 +30,7 @@
[units$]="units$"
(quantityChange)="changeQuantity($event)"
(locationChange)="changeMixLocation($event)"
(deduct)="deductMixQuantities($event)">
(deduct)="deductMix($event)">
</cre-mixes-card>
</div>

View File

@ -23,9 +23,10 @@ export class ExploreComponent extends ErrorHandlingComponent {
quantitiesChanges = new Map<number, Map<number, number>>()
mixesLocationChanges = new Map<number, string>()
deductedMixId: number | null
handledErrorModels: ErrorModel[] = [{
filter: error => error.status === 409,
consumer: error => this.deductErrorBody = error.error,
consumer: error => this.deductErrorBody = {mix: this.deductedMixId, lowQuantities: error.error.lowQuantities},
messageProducer: () => 'Certains produit ne sont pas en quantité suffisante dans l\'inventaire'
}]
@ -52,7 +53,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
this.note = r.note
if (this.recipe.mixes.length <= 0 || this.recipe.steps.length <= 0) {
this.alertService.pushWarning("Cette recette n'est pas complète")
this.alertService.pushWarning('Cette recette n\'est pas complète')
}
},
'/colors/list'
@ -60,7 +61,7 @@ export class ExploreComponent extends ErrorHandlingComponent {
}
ngOnDestroy() {
super.ngOnDestroy();
super.ngOnDestroy()
GlobalAlertHandlerComponent.extraTopMarginMultiplier = 0
}
@ -93,18 +94,22 @@ export class ExploreComponent extends ErrorHandlingComponent {
)
}
deductQuantities() {
this.performDeductQuantities(this.recipeService.deductQuantities(this.recipe, this.quantitiesChanges))
deductMix(mixId: number) {
this.deductedMixId = mixId
const firstMixMaterial = this.recipe.mixes.filter(m => m.id === mixId)[0].mixMaterials[0]
if (this.quantitiesChanges.has(mixId) && this.quantitiesChanges.get(mixId).has(firstMixMaterial.material.id)) {
const originalQuantity = firstMixMaterial.quantity
const currentQuantity = this.quantitiesChanges.get(mixId).get(firstMixMaterial.material.id)
this.subscribeDeductMix(this.recipeService.deductMixFromQuantities(mixId, originalQuantity, currentQuantity))
} else {
this.subscribeDeductMix(this.recipeService.deductMix(mixId, 1))
}
}
deductMixQuantities(mixId: number) {
this.performDeductQuantities(this.recipeService.deductMixQuantities(this.recipe, mixId, this.quantitiesChanges.get(mixId)))
}
performDeductQuantities(observable: Observable<void>) {
subscribeDeductMix(observable: Observable<any>) {
this.subscribe(
observable,
() => this.alertService.pushSuccess('Les quantités des produits utilisés ont été déduites de l\'inventaire'),
() => this.alertService.pushSuccess('Les quantités quantités ont été déduites de l\'inventaire'),
true
)
}

View File

@ -3,6 +3,7 @@ import {ApiService} from '../../shared/service/api.service'
import {Observable} from 'rxjs'
import {Recipe, RecipeStep} from '../../shared/model/recipe.model'
import {map} from 'rxjs/operators'
import {MaterialQuantity} from '../../material/service/inventory.service'
@Injectable({
providedIn: 'root'
@ -63,46 +64,12 @@ export class RecipeService {
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))
deductMixFromQuantities(mixId: number, originalQuantity: number, currentQuantity: number): Observable<MaterialQuantity[]> {
return this.deductMix(mixId, currentQuantity / originalQuantity)
}
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)
deductMix(mixId: number, ratio: number): Observable<MaterialQuantity[]> {
const body = {id: mixId, ratio}
return this.api.put<MaterialQuantity[]>('/inventory/deduct/mix', body)
}
}

View File

@ -94,13 +94,14 @@ export class AddComponent extends ErrorHandlingComponent {
submit(values) {
const permissionsField = currentPermissionsFieldComponent
if (permissionsField.valid()) {
const groupId = values.groupId >= 0 ? values.groupId : null
this.subscribeAndNavigate(
this.employeeService.save(
values.id,
values.firstName,
values.lastName,
values.password,
values.groupId,
groupId,
permissionsField.allEnabledPermissions
),
'/employee/list'

View File

@ -92,22 +92,23 @@ export class EditComponent extends ErrorHandlingComponent {
permissionsField.allEnabledPermissions
),
() => {
const group = values.groupId
if (group >= 0) {
this.subscribeAndNavigate(
this.groupService.addEmployeeToGroup(group, this.employee),
'/employee/list'
)
} else {
if (this.employee.group) {
this.subscribeAndNavigate(
this.groupService.removeEmployeeFromGroup(this.employee),
'/employee/list'
)
} else {
// TODO de-comment when backend will be ready
// const group = values.groupId
// if (group >= 0) {
// this.subscribeAndNavigate(
// this.groupService.addEmployeeToGroup(group, this.employee),
// '/employee/list'
// )
// } else {
// if (this.employee.group) {
// this.subscribeAndNavigate(
// this.groupService.removeEmployeeFromGroup(this.employee),
// '/employee/list'
// )
// } else {
this.urlUtils.navigateTo('/employee/list')
}
}
// }
// }
}
)
}

View File

@ -18,8 +18,7 @@ export class ListComponent extends ErrorHandlingComponent {
defaultGroup: EmployeeGroup = null
columns = [
{def: 'name', title: 'Nom', valueFn: g => g.name},
{def: 'permissionCount', title: 'Nombre de permissions', valueFn: g => g.permissions.length},
{def: 'employeeCount', title: 'Nombre d\'utilisateurs', valueFn: g => g.employeeCount}
{def: 'permissionCount', title: 'Nombre de permissions', valueFn: g => g.permissions.length}
]
buttons = [{
text: 'Définir par défaut',

View File

@ -18,8 +18,7 @@ export class GroupService {
.pipe(tap(groups => groups.unshift({
id: -1,
name: 'Aucun',
permissions: [],
employeeCount: 0
permissions: []
})))
}

View File

@ -1,12 +1,12 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ListComponent} from "./pages/list/list.component";
import {InventoryComponent} from "./pages/inventory/inventory.component";
import {AddComponent} from "./pages/add/add.component";
import {EditComponent} from "./pages/edit/edit.component";
const routes: Routes = [{
path: 'list',
component: ListComponent
component: InventoryComponent
}, {
path: 'add',
component: AddComponent

View File

@ -2,18 +2,24 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MaterialRoutingModule} from './material-routing.module';
import {ListComponent} from './pages/list/list.component';
import {InventoryComponent} from './pages/inventory/inventory.component';
import {SharedModule} from "../shared/shared.module";
import {AddComponent} from './pages/add/add.component';
import {EditComponent} from './pages/edit/edit.component';
import {ColorsModule} from '../colors/colors.module'
import {MatSortModule} from '@angular/material/sort'
import {FormsModule} from '@angular/forms'
@NgModule({
declarations: [ListComponent, AddComponent, EditComponent],
declarations: [InventoryComponent, AddComponent, EditComponent],
imports: [
CommonModule,
MaterialRoutingModule,
SharedModule
SharedModule,
ColorsModule,
MatSortModule,
FormsModule
]
})
export class MaterialModule {

View File

@ -0,0 +1,148 @@
<div class="action-bar backward">
<!-- Left -->
<div class="d-flex flex-row">
<mat-form-field class="mr-4">
<mat-label>Recherche par code...</mat-label>
<input
matInput
type="text"
[(ngModel)]="materialNameFilter"
(keyup)="filterDataSource()"/>
</mat-form-field>
<mat-form-field *ngIf="materialTypes$ | async as materialTypes">
<mat-label>Recherche par type de produit</mat-label>
<mat-select
[(value)]="materialTypeFilter"
(valueChange)="filterDataSource()">
<mat-option
*ngFor="let materialType of materialTypes"
[value]="materialType.id">
{{materialType.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Right -->
<div class="ml-auto">
<mat-form-field class="mr-4">
<mat-label>Quantité faible</mat-label>
<input
matInput
type="number"
step="0.01"
[(ngModel)]="lowQuantityThreshold"/>
</mat-form-field>
<cre-unit-selector [(unit)]="units"></cre-unit-selector>
<button
*ngIf="canEditMaterial"
class="ml-3"
mat-raised-button
color="accent"
routerLink="/material/add">
Ajouter
</button>
</div>
</div>
<table
mat-table
matSort
class="mx-auto"
[dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Code</th>
<td mat-cell *matCellDef="let material">{{material.name}}</td>
</ng-container>
<ng-container matColumnDef="materialType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type de produit</th>
<td mat-cell *matCellDef="let material">{{material.materialType.name}}</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef>Quantité</th>
<td mat-cell *matCellDef="let material">{{getQuantity(material)}} {{units}}</td>
</ng-container>
<ng-container matColumnDef="addQuantity">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canEditMaterial" *matCellDef="let material; let i = index">
<div *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material" class="input-group">
<input
#addQuantityInput
class="form-control"
type="number"
step="0.01"
placeholder="0"/>
<div class="input-group-append">
<button
mat-flat-button
color="accent"
(click)="addQuantity(material, addQuantityInput)">
Ajouter
</button>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="lowQuantityIcon">
<th mat-header-cell *matHeaderCellDef mat-sort-header></th>
<td mat-cell *matCellDef="let material" [class.disabled]="!isLowQuantity(material)">
<mat-icon
svgIcon="format-color-fill"
class="color-warning"
title="Il y a moins que {{lowQuantityThreshold}} {{units}} de {{material.name}} en inventaire">
</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="simdutIcon">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material" [class.disabled]="materialHasSimdut(material)">
<mat-icon
svgIcon="text-box-remove"
color="warn"
title="Ce produit n'a pas fiche signalitique">
</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="!canEditMaterial">
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
<button
mat-raised-button
color="accent"
routerLink="/material/edit/{{material.id}}">
Modifier
</button>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="openSimdutButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let material; let i = index" [class.disabled]="canEditMaterial">
<ng-container *ngIf="(!hoveredMaterial && i === 0) || hoveredMaterial === material">
<button
mat-raised-button
color="accent"
[disabled]="!materialHasSimdut(material)"
(click)="openSimdut(material)">
Fiche signalitique
</button>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr
mat-row
class="entity-row"
*matRowDef="let material; columns: columns"
(mouseover)="hoveredMaterial = material">
</tr>
</table>

View File

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

View File

@ -0,0 +1,128 @@
import {Component, ViewChild} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {MaterialService} from '../../service/material.service'
import {EmployeePermission} from '../../../shared/model/employee'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service'
import {Material} from '../../../shared/model/material.model'
import {AccountService} from '../../../accounts/services/account.service'
import {convertQuantity, UNIT_MILLILITER} from '../../../shared/units'
import {MatSort} from '@angular/material/sort'
import {MatTableDataSource} from '@angular/material/table'
import {MaterialTypeService} from '../../../material-type/service/material-type.service'
import {InventoryService} from '../../service/inventory.service'
import {environment} from '../../../../../environments/environment'
@Component({
selector: 'cre-list',
templateUrl: './inventory.component.html',
styleUrls: ['./inventory.component.sass']
})
export class InventoryComponent extends ErrorHandlingComponent {
@ViewChild(MatSort) sort: MatSort
materials: Material[] | null
materialTypes$ = this.materialTypeService.all
dataSource: MatTableDataSource<Material>
hasSimdut: any
columns = ['name', 'materialType', 'quantity', 'addQuantity', 'lowQuantityIcon', 'simdutIcon', 'editButton', 'openSimdutButton']
hoveredMaterial: Material | null
units = UNIT_MILLILITER
lowQuantityThreshold = 100 // TEMPORARY will be in the application settings
materialTypeFilter = 1
materialNameFilter = ''
constructor(
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
private inventoryService: InventoryService,
private accountService: AccountService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.subscribe(
this.materialService.allNotMixType,
materials => {
this.materials = materials
this.dataSource = this.setupDataSource()
},
true,
1
)
this.subscribe(
this.materialService.getSimduts(),
ids => this.hasSimdut = ids,
false,
1
)
}
setupDataSource(): MatTableDataSource<Material> {
this.dataSource = new MatTableDataSource<Material>(this.materials)
this.dataSource.sortingDataAccessor = (material, header) => {
switch (header) {
case 'materialType':
return material[header].name
case 'lowQuantityIcon':
return this.isLowQuantity(material)
default:
return material[header]
}
}
this.dataSource.filterPredicate = (material, filter) => {
return (!this.materialTypeFilter || this.materialTypeFilter === 1 || material.materialType.id === this.materialTypeFilter) &&
(!this.materialNameFilter || material.name.toLowerCase().includes(this.materialNameFilter.toLowerCase()))
}
this.dataSource.sort = this.sort
return this.dataSource
}
filterDataSource() {
this.dataSource.filter = 'filter'
}
isLowQuantity(material: Material) {
return this.getQuantity(material) < this.lowQuantityThreshold
}
getQuantity(material: Material): number {
return Math.round(convertQuantity(material.inventoryQuantity, UNIT_MILLILITER, this.units) * 100) / 100
}
materialHasSimdut(material: Material): boolean {
return this.hasSimdut && this.hasSimdut.filter(i => i === material.id).length > 0
}
openSimdut(material: Material) {
window.open(`${environment.apiUrl}/material/${material.id}/simdut`, '_blank')
}
addQuantity(material: Material, input: HTMLInputElement) {
const quantity = input.valueAsNumber
this.subscribe(
this.inventoryService.add(material.id, quantity),
materialQuantities => {
// Reset the input value
input.value = null
material.inventoryQuantity = parseInt(materialQuantities.filter(mq => mq.material === material.id)[0].quantity)
},
true
)
}
get canEditMaterial(): boolean {
return this.accountService.hasPermission(EmployeePermission.EDIT_MATERIAL)
}
}

View File

@ -1,8 +0,0 @@
<cre-entity-list
[entities$]="materials$"
[columns]="columns"
[icons]="icons"
[buttons]="buttons"
addLink="/catalog/material/add"
addPermission="EDIT_MATERIAL">
</cre-entity-list>

View File

@ -1,69 +0,0 @@
import {Component} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {MaterialService} from '../../service/material.service'
import {EmployeePermission} from '../../../shared/model/employee'
import {environment} from '../../../../../environments/environment'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorService} from '../../../shared/service/error.service'
@Component({
selector: 'cre-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
materials$ = this.materialService.allNotMixType
columns = [
{def: 'name', title: 'Code', valueFn: t => t.name},
{def: 'inventoryQuantity', title: 'Quantité', valueFn: t => t.inventoryQuantity},
{def: 'materialType', title: 'Type de produit', valueFn: t => t.materialType.name}
]
icons = [{
icon: 'text-box-remove',
color: 'warn',
title: 'Ce produit n\'a pas de fiche signalitique',
disabledFn: t => this.hasSimdutMap[t.id]
}]
buttons = [{
text: 'Modifier',
linkFn: t => `/catalog/material/edit/${t.id}`,
permission: EmployeePermission.EDIT_MATERIAL
}, {
text: 'Fiche signalitique',
linkFn: t => {
return {
externalLink: environment.apiUrl + `/material/${t.id}/simdut`
}
},
disabledFn: t => !this.hasSimdutMap[t.id]
}]
private hasSimdutMap: any = {}
constructor(
private materialService: MaterialService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.subscribe(
this.materials$,
mArray => {
mArray.forEach(m => {
this.subscribe(
this.materialService.hasSimdut(m.id),
b => this.hasSimdutMap[m.id] = b
)
})
},
false,
1
)
}
}

View File

@ -0,0 +1,29 @@
import {Injectable} from '@angular/core'
import {ApiService} from '../../shared/service/api.service'
import {Observable} from 'rxjs'
@Injectable({
providedIn: 'root'
})
export class InventoryService {
constructor(
private api: ApiService
) {
}
add(material: number, quantity: number): Observable<MaterialQuantity[]> {
return this.api.put<MaterialQuantity[]>('/material/inventory/add', [{material, quantity}])
}
deduct(materialQuantities: [{material: number, quantity: number}]): Observable<MaterialQuantity[]> {
return this.api.put<MaterialQuantity[]>('/material/inventory/deduct', materialQuantities)
}
}
export class MaterialQuantity {
constructor(
public material: number,
public quantity: number
) {
}
}

View File

@ -37,6 +37,10 @@ export class MaterialService {
return this.api.get<boolean>(`/material/${id}/simdut/exists`)
}
getSimduts(): Observable<number[]> {
return this.api.get<number[]>('/material/simdut')
}
save(name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable<void> {
const body = new FormData()
body.append('name', name)

View File

@ -15,8 +15,7 @@ export class EmployeeGroup {
constructor(
public id: number,
public name: string,
public permissions: EmployeePermission[],
public employeeCount: number
public permissions: EmployeePermission[]
) {
}
}

View File

@ -11,7 +11,7 @@ import {GlobalAlertHandlerComponent} from '../../modules/shared/components/globa
export class CatalogComponent implements OnInit, OnDestroy {
links: NavLink[] = [
{route: '/catalog/materialtype', title: 'Types de produit', permission: EmployeePermission.VIEW_MATERIAL_TYPE},
{route: '/catalog/material', title: 'Produits', permission: EmployeePermission.VIEW_MATERIAL},
{route: '/catalog/material', title: 'Inventaire', permission: EmployeePermission.VIEW_MATERIAL},
{route: '/catalog/company', title: 'Bannières', permission: EmployeePermission.VIEW_COMPANY}
]