import {Component, inject, OnInit, ViewChild} from '@angular/core'; import {CommonModule} from '@angular/common'; import { MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatNoDataRow, MatTable, MatTableDataSource } from '@angular/material/table'; import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator'; import {MatSort, MatSortModule} from '@angular/material/sort'; import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatInput} from '@angular/material/input'; import {MatButton, MatIconButton} from '@angular/material/button'; import {MatIcon} from '@angular/material/icon'; import {FormBuilder, ReactiveFormsModule, FormsModule} from '@angular/forms'; import {MatDialog, MatDialogModule} from '@angular/material/dialog'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {forkJoin, finalize} from 'rxjs'; import {SelectionModel} from '@angular/cdk/collections'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {PsItem} from '../../interfaces/ps-item'; import {ProductListItem} from '../../interfaces/product-list-item'; import {PrestashopService} from '../../services/prestashop.serivce'; import {ProductDialogData, PsProductDialogComponent} from '../ps-product-dialog/ps-product-dialog.component'; @Component({ selector: 'app-ps-product-crud', standalone: true, templateUrl: './ps-product-crud.component.html', styleUrls: ['./ps-product-crud.component.css'], imports: [ CommonModule, ReactiveFormsModule, FormsModule, MatTable, MatColumnDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatNoDataRow, MatSortModule, MatPaginatorModule, MatFormField, MatLabel, MatInput, MatButton, MatIconButton, MatIcon, MatDialogModule, MatProgressSpinnerModule, MatCheckboxModule ] }) export class PsProductCrudComponent implements OnInit { private readonly fb = inject(FormBuilder); private readonly ps = inject(PrestashopService); private readonly dialog = inject(MatDialog); categories: PsItem[] = []; manufacturers: PsItem[] = []; suppliers: PsItem[] = []; private catMap = new Map(); private manMap = new Map(); private supMap = new Map(); // added 'select' column first displayed: string[] = ['select', 'id', 'name', 'category', 'manufacturer', 'supplier', 'priceTtc', 'quantity', 'actions']; dataSource = new MatTableDataSource([]); @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; @ViewChild(MatTable) table!: MatTable; // selection model (multiple) selection = new SelectionModel(true, []); filterCtrl = this.fb.control(''); isLoading = false; ngOnInit(): void { forkJoin({ cats: this.ps.list('categories'), mans: this.ps.list('manufacturers'), sups: this.ps.list('suppliers') }).subscribe({ next: ({cats, mans, sups}) => { this.categories = cats ?? []; this.catMap = new Map(this.categories.map(x => [x.id, x.name])); this.manufacturers = mans ?? []; this.manMap = new Map(this.manufacturers.map(x => [x.id, x.name])); this.suppliers = sups ?? []; this.supMap = new Map(this.suppliers.map(x => [x.id, x.name])); this.reload(); }, error: err => { console.error('Erreur lors du chargement des référentiels', err); } }); this.filterCtrl.valueChanges.subscribe(v => { this.dataSource.filter = (v ?? '').toString().trim().toLowerCase(); if (this.paginator) this.paginator.firstPage(); }); this.dataSource.filterPredicate = (row: any, f: string) => row.name?.toLowerCase().includes(f) || String(row.id).includes(f) || (row.categoryName?.toLowerCase().includes(f)) || (row.manufacturerName?.toLowerCase().includes(f)) || (row.supplierName?.toLowerCase().includes(f)) || String(row.quantity ?? '').includes(f); } private toTtc(ht: number, vat: number) { return Math.round(((ht * (1 + vat)) + Number.EPSILON) * 100) / 100; } private attachSortingAccessors() { this.dataSource.sortingDataAccessor = (item: any, property: string) => { switch (property) { case 'category': return (item.categoryName ?? '').toLowerCase(); case 'manufacturer': return (item.manufacturerName ?? '').toLowerCase(); case 'supplier': return (item.supplierName ?? '').toLowerCase(); case 'priceTtc': return Number(item.priceTtc ?? 0); case 'name': return (item.name ?? '').toLowerCase(); default: return item[property]; } }; this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } private bindProducts(p: (ProductListItem & { priceHt?: number })[]) { const vat = 0.2; // clear selection because objects will be new after reload this.selection.clear(); this.dataSource.data = p.map(x => ({ ...x, categoryName: x.id_category_default ? (this.catMap.get(x.id_category_default) ?? '') : '', manufacturerName: x.id_manufacturer ? (this.manMap.get(x.id_manufacturer) ?? '') : '', supplierName: x.id_supplier ? (this.supMap.get(x.id_supplier) ?? '') : '', priceTtc: this.toTtc(x.priceHt ?? 0, vat) })); this.attachSortingAccessors(); this.table?.renderRows?.(); } reload() { this.isLoading = true; this.ps.listProducts() .pipe( finalize(() => { this.isLoading = false; }) ) .subscribe({ next: p => this.bindProducts(p), error: err => { console.error('Erreur lors du chargement des produits', err); } }); } create() { if (this.isLoading) return; const data: ProductDialogData = { mode: 'create', refs: { categories: this.categories, manufacturers: this.manufacturers, suppliers: this.suppliers } }; this.dialog.open(PsProductDialogComponent, {width: '900px', data}) .afterClosed() .subscribe(ok => { if (ok) this.reload(); }); } edit(row: ProductListItem & { priceHt?: number }) { if (this.isLoading) return; const data: ProductDialogData = { mode: 'edit', productRow: row, refs: { categories: this.categories, manufacturers: this.manufacturers, suppliers: this.suppliers } }; this.dialog.open(PsProductDialogComponent, {width: '900px', data}) .afterClosed() .subscribe(ok => { if (ok) this.reload(); }); } remove(row: ProductListItem) { if (this.isLoading) return; if (!confirm(`Supprimer le produit "${row.name}" (#${row.id}) ?`)) return; this.isLoading = true; this.ps.deleteProduct(row.id) .pipe( finalize(() => { }) ) .subscribe({ next: () => this.reload(), error: (e: unknown) => { this.isLoading = false; alert('Erreur: ' + (e instanceof Error ? e.message : String(e))); } }); } // --- Selection helpers --- private getVisibleRows(): any[] { const data = this.dataSource.filteredData || []; if (!this.paginator) return data; const start = this.paginator.pageIndex * this.paginator.pageSize; return data.slice(start, start + this.paginator.pageSize); } isAllSelected(): boolean { const visible = this.getVisibleRows(); return visible.length > 0 && visible.every(r => this.selection.isSelected(r)); } isAnySelected(): boolean { return this.selection.hasValue(); } masterToggle(checked: boolean) { const visible = this.getVisibleRows(); if (checked) { visible.forEach(r => this.selection.select(r)); } else { visible.forEach(r => this.selection.deselect(r)); } } toggleSelection(row: any, checked: boolean) { if (checked) this.selection.select(row); else this.selection.deselect(row); } deleteSelected() { if (this.isLoading) return; const ids = this.selection.selected.map(s => s.id); if (!ids.length) return; if (!confirm(`Supprimer ${ids.length} produit(s) sélectionné(s) ?`)) return; this.isLoading = true; const calls = ids.map((id: number) => this.ps.deleteProduct(id)); forkJoin(calls).pipe(finalize(() => { // nothing extra, reload will clear selection })).subscribe({ next: () => this.reload(), error: (e: unknown) => { this.isLoading = false; alert('Erreur: ' + (e instanceof Error ? e.message : String(e))); } }); } }