Added checkbox in stock page

This commit is contained in:
2026-05-26 18:35:44 +01:00
parent 4dd1d7e81d
commit 961551e926
8 changed files with 310 additions and 189 deletions
@@ -0,0 +1,40 @@
<form [formGroup]="addProductForm">
<nz-form-item nz-flex>
<nz-form-label nzSpan="12" nzRequired>
Fournisseur
</nz-form-label>
<nz-form-control nzSpan="12" nzErrorTip="Ce champ est requis">
<nz-select formControlName="supplierId" nzPlaceHolder="Choisir un fournisseur" nzShowSearch>
@for (supplier of suppliers(); track supplier.id){
<nz-option [nzLabel]="supplier.name" [nzValue]="supplier.id"></nz-option>
}
</nz-select>
</nz-form-control>
</nz-form-item>
<div class="overflow-x-auto mt-6">
<nz-table [nzBordered]="true" class="mx-auto text-center">
<thead>
<tr>
<th class="text-center">Produit</th>
<th class="text-center">Prix</th>
</tr>
</thead>
<tbody formArrayName="lines">
@for (line of lines.controls; let i = $index; track i) {
<tr [formGroupName]="i" class="text-center">
<td class="text-center">{{ line.value.name }}</td>
<td class="text-center">
<nz-input-number
formControlName="price"
[nzMin]="1"
[nzStep]="1">
</nz-input-number>
</td>
</tr>
}
</tbody>
</nz-table>
</div>
</form>
@@ -0,0 +1,68 @@
import {Component, inject, OnInit, signal} from '@angular/core';
import {FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {GetProductDto, GetSupplierDto, SuppliersService} from "../../services/api";
import {NzColDirective, NzRowDirective} from "ng-zorro-antd/grid";
import {NzFormControlComponent, NzFormLabelComponent} from "ng-zorro-antd/form";
import {NzFlexDirective} from "ng-zorro-antd/flex";
import {NzTableComponent} from "ng-zorro-antd/table";
import {NzInputNumberComponent} from "ng-zorro-antd/input-number";
import {NzOptionComponent, NzSelectComponent} from "ng-zorro-antd/select";
import {firstValueFrom} from "rxjs";
import {NzNotificationService} from "ng-zorro-antd/notification";
@Component({
selector: 'app-add-product-supplier-form',
imports: [
NzRowDirective,
NzFormControlComponent,
NzFormLabelComponent,
ReactiveFormsModule,
NzFlexDirective,
NzColDirective,
NzTableComponent,
NzInputNumberComponent,
NzOptionComponent,
NzSelectComponent
],
templateUrl: './add-product-supplier-form.html',
styleUrl: './add-product-supplier-form.css',
})
export class AddProductSupplierForm implements OnInit {
addProductForm: FormGroup = new FormGroup({
supplierId: new FormControl<number>(null, Validators.required),
lines: new FormArray([], Validators.required),
});
private suppliersServices = inject(SuppliersService);
private notificationService = inject(NzNotificationService);
suppliers = signal<GetSupplierDto[]>([]);
async ngOnInit() {
try {
const suppliers = await firstValueFrom(this.suppliersServices.getAllSuppliersEndpoint());
this.suppliers.set(suppliers);
}
catch {
this.notificationService.error('Erreur', 'Erreur de communication avec l\'API');
}
}
get lines(): FormArray {
return this.addProductForm.get('lines') as FormArray;
}
addProductToForm(selectedProducts: GetProductDto[]) {
this.lines.clear();
selectedProducts.forEach(p => {
this.lines.push(
new FormGroup({
productId: new FormControl(p.id),
name: new FormControl(p.name),
price: new FormControl(1, [Validators.required,Validators.min(0)])
})
);
});
}
}
@@ -1,18 +1,16 @@
<nz-table
[nzData]="filteredProducts()"
[nzData]="products()"
[nzFrontPagination]="false"
[nzLoading]="productsLoading()"
(nzCurrentPageDataChange)="onCurrentPageDataChange($event)"
>
<thead>
<tr class="text-center">
<th nzWidth="40px">
<label
nz-checkbox
[(ngModel)]="checked"
[nzIndeterminate]="indeterminate"
(ngModelChange)="onAllChecked($event)"
></label>
<label nz-checkbox
[(ngModel)]="checked"
[nzIndeterminate]="indeterminate"
(ngModelChange)="allCheck($event)">
</label>
</th>
<th>Nom</th>
<th>Référence</th>
@@ -27,16 +25,15 @@
</thead>
<tbody class="text-center">
@for (product of filteredProducts(); track product.id) {
@for (product of products(); track product.id) {
<tr>
<td nzWidth="40px">
<label
nz-checkbox
[ngModel]="setOfCheckedId.has(product.id)"
[ngModel]="selectedIds.includes(product.id)"
(ngModelChange)="onItemChecked(product.id, $event)"
></label>
</td>
<td>{{ product.name }}</td>
<td>{{ product.references }}</td>
<td>{{ product.nec }}</td>
+70 -121
View File
@@ -1,14 +1,14 @@
import {Component, computed, inject, OnInit, output, signal, viewChild} from '@angular/core';
import {Component, inject, OnInit, output, signal, viewChild} from '@angular/core';
import {NzTableComponent, NzThMeasureDirective} from "ng-zorro-antd/table";
import {ModalNav} from "../modal-nav/modal-nav";
import {NzIconDirective} from "ng-zorro-antd/icon";
import {StockForm} from "../stock-form/stock-form";
import {NzDividerComponent} from "ng-zorro-antd/divider";
import {FormsModule} from "@angular/forms";
import {NzCheckboxComponent} from "ng-zorro-antd/checkbox";
import {GetProductDto, ProductsService, WarehouseproductsService} from "../../services/api";
import {NzNotificationService} from "ng-zorro-antd/notification";
import {firstValueFrom} from "rxjs";
import {first, firstValueFrom} from "rxjs";
import {NzCheckboxComponent} from "ng-zorro-antd/checkbox";
interface ProductWithQuantity extends GetProductDto {
totalQuantity?: number;
@@ -23,8 +23,8 @@ interface ProductWithQuantity extends GetProductDto {
StockForm,
NzDividerComponent,
FormsModule,
NzCheckboxComponent,
NzThMeasureDirective,
NzCheckboxComponent,
],
templateUrl: './stock-table.html',
styleUrl: './stock-table.css',
@@ -34,109 +34,49 @@ export class StockTable implements OnInit {
private productsService = inject(ProductsService);
private wareHousseProductsService = inject(WarehouseproductsService)
private notificationService = inject(NzNotificationService)
products = signal<ProductWithQuantity[]>([]);
productsLoading = signal<boolean>(false);
updateProduct = viewChild.required<StockForm>('stockForm');
modal = viewChild.required<ModalNav>('modalNav');
checked = false;
indeterminate = false;
setOfCheckedId = new Set<number>();
selectionChange = output<boolean>()
currentPageData: GetProductDto[] = [];
selectionChange = output<number[]>();
productsTables = output<ProductWithQuantity[]>();
private searchQuery = signal<string>('');
filteredProducts = computed(() => {
const q = this.searchQuery().toLowerCase().trim();
if (!q) return this.products();
return this.products().filter(s => {
const name = (s.name ?? '').toLowerCase();
return name.includes(q);
});
});
applySearch(query: string) {
this.searchQuery.set(query);
}
get hasSelection(): boolean {
return this.setOfCheckedId.size > 0;
}
updateCheckedSet(id: number, checked: boolean): void {
if (checked) this.setOfCheckedId.add(id);
else this.setOfCheckedId.delete(id);
}
onItemChecked(id: number, checked: boolean): void {
this.updateCheckedSet(id, checked);
this.refreshCheckedStatus();
}
onAllChecked(checked: boolean): void {
this.currentPageData.forEach(item =>
this.updateCheckedSet(item.id, checked)
);
this.refreshCheckedStatus();
}
onCurrentPageDataChange($event: GetProductDto[]): void {
this.currentPageData = $event;
this.refreshCheckedStatus();
}
refreshCheckedStatus(): void {
const total = this.currentPageData.length;
const checkedCount = this.currentPageData.filter(p => this.setOfCheckedId.has(p.id)).length;
this.checked = checkedCount === total;
this.indeterminate = checkedCount > 0 && checkedCount < total;
setTimeout(() => this.selectionChange.emit(this.hasSelection));
}
get selectedIds() {
return Array.from(this.setOfCheckedId);
}
selectedProduct: GetProductDto | null = null;
checked: boolean = false;
indeterminate: boolean = false;
selectedIds: number[] = [];
async ngOnInit() {
await this.fetchProducts();
}
async fetchTotalQuantity(product: ProductWithQuantity) {
try {
const res = await firstValueFrom(
this.wareHousseProductsService.getTotalQuantityEndpoint(product.id)
);
product.totalQuantity = res.totalQuantity;
} catch (e) {
product.totalQuantity = 0;
}
}
async fetchProducts() {
this.productsLoading.set(true);
try {
const products = await firstValueFrom(this.productsService.getAllProductsEndpoint());
// transforme chaque produit en ProductWithQuantity
const productsWithQuantity: ProductWithQuantity[] = products.map(p => ({...p}));
this.products.set(productsWithQuantity);
// récupérer la quantité pour chaque produit en parallèle
await Promise.all(productsWithQuantity.map(p => this.fetchTotalQuantity(p)));
// déclencher la mise à jour du signal
this.products.set([...productsWithQuantity]);
} catch (e) {
this.notificationService.error(
'Erreur',
'Erreur de communication avec l\'API'
const productsWithQuantity = await Promise.all(
products.map(async (x) => {
try {
const quantity = await firstValueFrom(this.wareHousseProductsService.getTotalQuantityEndpoint(x.id));
return {
...x,
totalQuantity: quantity.totalQuantity ?? 0
};
} catch {
return {
...x,
totalQuantity: 0
};
}
})
);
this.products.set(productsWithQuantity);
this.productsTables.emit(productsWithQuantity);
} catch {
this.notificationService.error('Erreur', 'Erreur de communication avec l\'API');
}
this.productsLoading.set(false);
}
@@ -144,48 +84,28 @@ export class StockTable implements OnInit {
async delete(productId: number) {
try {
await firstValueFrom(this.productsService.deleteProductEndpoint(productId))
this.notificationService.success(
'Success',
'Suppression effectuée'
)
} catch (e) {
this.notificationService.error(
'Erreur',
'Impossible de supprimer la ligne'
)
this.notificationService.success('Success', 'Suppression effectuée');
} catch {
this.notificationService.error('Erreur', 'Impossible de supprimer la ligne');
}
await this.fetchProducts();
}
async edit(id: number, updateProductComponent: StockForm) {
if (updateProductComponent.stockForm.invalid) {
this.notificationService.error(
'Erreur',
'Erreur d\'écriture dans le formulaire'
)
this.notificationService.error('Erreur', 'Erreur d\'écriture dans le formulaire')
return;
}
try {
const products = updateProductComponent.stockForm.getRawValue();
await firstValueFrom(this.productsService.patchProductMinimalStockEndpoint(id, products))
this.notificationService.success(
'Success',
'Limite de stock modifiée'
)
} catch (e) {
console.error(e);
this.notificationService.error(
'Erreur',
'Erreur lors de la modification'
)
await firstValueFrom(this.productsService.patchProductMinimalStockEndpoint(id, products));
await this.fetchProducts();
this.notificationService.success('Success', 'Limite de stock modifiée');
} catch {
this.notificationService.error('Erreur', 'Erreur lors de la modification');
}
}
selectedProduct: GetProductDto | null = null;
openEditModal(product: GetProductDto) {
this.selectedProduct = {...product};
this.modal().showModal();
@@ -193,7 +113,6 @@ export class StockTable implements OnInit {
async onModalOk(productId: number, updateProductComponent: StockForm, modal: ModalNav) {
if (!this.selectedProduct) return;
await this.edit(productId, updateProductComponent);
updateProductComponent.stockForm.reset();
modal.isVisible = false;
@@ -203,4 +122,34 @@ export class StockTable implements OnInit {
onModalCancel(modal: ModalNav) {
modal.isVisible = false;
}
updateCheck(id: number, checked: boolean) {
if (checked) {
if (!this.selectedIds.includes(id)) {
this.selectedIds.push(id);
}
} else this.selectedIds = this.selectedIds.filter(x => x !== id);
}
refreshCheckStatus() {
const total = this.products().length;
const checkedCount = this.selectedIds.length;
this.checked = checkedCount === total;
this.indeterminate = checkedCount > 0 && checkedCount < total;
this.selectionChange.emit(this.selectedIds);
}
allCheck(checked: boolean) {
this.products().forEach(x =>
this.updateCheck(x.id, checked)
);
this.refreshCheckStatus();
}
onItemChecked(id: number, checked: boolean): void {
this.updateCheck(id, checked);
this.refreshCheckStatus();
}
}
@@ -22,6 +22,8 @@ import {GetSupplierDto} from "../../services/api";
styleUrl: './supplier-form.css',
})
export class SupplierForm {
supplier = input<GetSupplierDto>();
supplierForm: FormGroup = new FormGroup({
name: new FormControl<string>(null, [Validators.required]),
email: new FormControl<string>(null, [Validators.required]),
@@ -33,8 +35,6 @@ export class SupplierForm {
})
supplier = input<GetSupplierDto>();
constructor() {
effect(() => {
if (this.supplier()) {
+17 -4
View File
@@ -1,9 +1,9 @@
<div class="flex mt-2">
@if (hasSelection) {
@if (productIds.length) {
<app-modal-button #modalButtonPurchaseOrder
(click)="openPurchaseOrderForm()"
(ok)="onModalOk()"
(cancel)="onModalCancel()"
(ok)="onModalPurchaseOrderOk()"
(cancel)="onModalPurchaseOrderCancel()"
type="default"
name="Créer un bon de commande"
size="35%"
@@ -21,9 +21,22 @@
(cancel)="onModalQuotationCancel()">
<app-create-quotation-form #quotationForm></app-create-quotation-form>
</app-modal-button>
<app-modal-button #modalButtonSupplier
type="default"
name="Associer à un fournisseur"
size="35%"
class="ml-4"
(click)="openSupplierForm()"
(ok)="onModalSupplierOk()"
(cancel)="onModalSupplierCancel()">
<app-add-product-supplier-form #supplierForm></app-add-product-supplier-form>
</app-modal-button>
}
</div>
<div class="mt-4">
<app-stock-table #stockTable (selectionChange)="onSelectionChange($event)"></app-stock-table>
<app-stock-table (selectionChange)="productIds = $event"
(productsTables)="products.set($event)">
</app-stock-table>
</div>
+105 -51
View File
@@ -1,11 +1,12 @@
import {Component, inject, viewChild} from '@angular/core';
import {Component, inject, signal, viewChild} from '@angular/core';
import {StockTable} from "../../components/stock-table/stock-table";
import {ModalButton} from "../../components/modal-button/modal-button";
import {PurchaseordersService, QuotationsService} from "../../services/api";
import {NzNotificationService} from "ng-zorro-antd/notification";
import {firstValueFrom} from "rxjs";
import {CreatePurchaseorderForm} from "../../components/create-purchaseorder-form/create-purchaseorder-form";
import {NzNotificationService} from "ng-zorro-antd/notification";
import {CreatePriceDto, PurchaseordersService, QuotationsService, SuppliersService} from "../../services/api";
import {firstValueFrom} from "rxjs";
import {CreateQuotationForm} from "../../components/create-quotation-form/create-quotation-form";
import {AddProductSupplierForm} from "../../components/add-product-supplier-form/add-product-supplier-form";
@Component({
selector: 'app-stock',
@@ -14,6 +15,7 @@ import {CreateQuotationForm} from "../../components/create-quotation-form/create
ModalButton,
CreatePurchaseorderForm,
CreateQuotationForm,
AddProductSupplierForm,
],
templateUrl: './stock.html',
styleUrl: './stock.css',
@@ -22,112 +24,164 @@ import {CreateQuotationForm} from "../../components/create-quotation-form/create
export class Stock {
createPurchaseOrder = viewChild.required<CreatePurchaseorderForm>('purchaseOrderForm');
createQuotation = viewChild.required<CreateQuotationForm>('quotationForm');
productTable = viewChild.required<StockTable>('stockTable');
private purchaseordersService = inject(PurchaseordersService)
private quotationsService = inject(QuotationsService)
private notificationService = inject(NzNotificationService)
addProduct = viewChild.required<AddProductSupplierForm>('supplierForm');
modalButtonPurchaseOrder = viewChild.required<ModalButton>('modalButtonPurchaseOrder');
modalButtonQuotation = viewChild.required<ModalButton>('modalButtonQuotation');
modalButtonSupplier = viewChild.required<ModalButton>('modalButtonSupplier');
hasSelection = false;
private purchaseOrdersService = inject(PurchaseordersService);
private quotationsService = inject(QuotationsService);
private suppliersService = inject(SuppliersService);
private notificationService = inject(NzNotificationService);
onSelectionChange(value: boolean) {
this.hasSelection = value;
products = signal([]);
productIds = [];
getSelectedProducts() {
return this.products().filter(x => this.productIds.includes(x.id));
}
openPurchaseOrderForm() {
this.createPurchaseOrder().syncSelectedProducts(this.getSelectedProducts());
}
openQuotationForm() {
this.createQuotation().syncSelectedProducts(this.getSelectedProducts());
}
openSupplierForm() {
this.addProduct().addProductToForm(this.getSelectedProducts());
}
async addPurchaseOrder() {
const form = this.createPurchaseOrder().createPurchaseOrderForm;
if (form.invalid) {
this.notificationService.error('Erreur', 'Formulaire invalide');
return;
}
const orderLines = this.createPurchaseOrder().lines.value.map(line => ({
productId: line.productId,
quantity: line.quantity
}));
if (orderLines.length === 0) {
if (!orderLines.length) {
this.notificationService.error('Erreur', 'Aucun produit sélectionné');
return;
}
const purchaseOrder = {
purchaseConditions: form.get('purchaseConditions')!.value,
products: orderLines
};
try {
await firstValueFrom(
this.purchaseordersService.createPurchaseOrder(purchaseOrder)
);
await firstValueFrom(this.purchaseOrdersService.createPurchaseOrder(purchaseOrder));
this.notificationService.success('Succès', 'Bon de commande créé');
} catch (e) {
} catch {
this.notificationService.error('Erreur', 'Erreur lors de la création du bon de commande.');
}
}
async onModalOk() {
await this.addPurchaseOrder();
this.createPurchaseOrder().createPurchaseOrderForm.reset();
this.modalButtonPurchaseOrder().isVisible = false;
await this.productTable().fetchProducts();
}
onModalCancel() {
this.modalButtonPurchaseOrder().isVisible = false;
}
openPurchaseOrderForm() {
const selectedProducts = this.productTable().products().filter(p =>
this.productTable().selectedIds.includes(p.id)
);
this.createPurchaseOrder().syncSelectedProducts(selectedProducts);
}
async addQuotation() {
if (this.createQuotation().createQuotationForm.invalid) {
const form = this.createQuotation().createQuotationForm;
if (form.invalid) {
this.notificationService.error('Erreur', 'Formulaire invalide');
return;
}
const orderLines = this.createQuotation().lines.value.map(line => ({
productId: line.productId,
quantity: line.quantity
}));
if (orderLines.length === 0) {
if (!orderLines.length) {
this.notificationService.error('Erreur', 'Aucun produit sélectionné');
return;
}
const quotation = {
message: this.createQuotation().createQuotationForm.get('message')!.value,
purchaseConditions: this.createQuotation().createQuotationForm.get('purchaseConditions')!.value,
products: orderLines
};
try {
await firstValueFrom(
this.quotationsService.createQuotationEndpoint(quotation)
);
this.notificationService.success('Succès', 'Devis créé');
} catch (e) {
console.log(this.createQuotation());
this.notificationService.error('Erreur', 'Erreur lors de la création du devis.');
await firstValueFrom(this.quotationsService.createQuotationEndpoint(quotation));
this.notificationService.success('Succès', 'Bon de commande créé');
} catch {
this.notificationService.error('Erreur', 'Erreur lors de la création du bon de commande.');
}
}
async addProductFromSupplier() {
const form = this.addProduct().addProductForm;
let success = 0;
if (form.invalid) {
this.notificationService.error('Erreur', 'Formulaire invalide');
return;
}
const supplierId = form.value.supplierId;
const lines = this.addProduct().lines.value;
if (!lines.length) {
this.notificationService.error('Erreur', 'Aucun produit sélectionné');
return;
}
for (const line of lines) {
try {
await firstValueFrom(
this.suppliersService.addProductToSupplierEndpoint(
supplierId,
line.productId,
{
sellingPrice: line.price
}
)
);
success++;
} catch {}
}
this.notificationService.success('Succès', `${success} produits ajoutés`);
}
async onModalPurchaseOrderOk() {
await this.addPurchaseOrder();
this.createPurchaseOrder().createPurchaseOrderForm.reset();
this.modalButtonPurchaseOrder().isVisible = false;
this.onModalPurchaseOrderCancel();
}
onModalPurchaseOrderCancel() {
this.modalButtonPurchaseOrder().isVisible = false;
}
async onModalQuotationOk() {
console.log(this.createQuotation().createQuotationForm.getRawValue());
await this.addQuotation();
this.createQuotation().createQuotationForm.reset();
this.modalButtonQuotation().isVisible = false;
await this.productTable().fetchProducts();
this.onModalQuotationCancel();
}
onModalQuotationCancel() {
this.modalButtonQuotation().isVisible = false;
}
openQuotationForm() {
const selectedProducts = this.productTable().products().filter(p =>
this.productTable().selectedIds.includes(p.id)
);
this.createQuotation().syncSelectedProducts(selectedProducts);
async onModalSupplierOk() {
await this.addProductFromSupplier();
this.addProduct().addProductForm.reset();
this.modalButtonSupplier().isVisible = false;
this.onModalSupplierCancel();
}
onModalSupplierCancel() {
this.modalButtonSupplier().isVisible = false;
}
}