diff --git a/client/src/app/components/ps-product-crud/ps-product-crud.component.html b/client/src/app/components/ps-product-crud/ps-product-crud.component.html index c1ebf79..d1606f6 100644 --- a/client/src/app/components/ps-product-crud/ps-product-crud.component.html +++ b/client/src/app/components/ps-product-crud/ps-product-crud.component.html @@ -7,6 +7,16 @@ add Nouveau produit + @if (selection.hasValue()) { + + } + Filtrer -
- -
- -
+ @if (isLoading) { +
+ +
+ } + + + + + + + + diff --git a/client/src/app/components/ps-product-crud/ps-product-crud.component.ts b/client/src/app/components/ps-product-crud/ps-product-crud.component.ts index c435b38..9e16e99 100644 --- a/client/src/app/components/ps-product-crud/ps-product-crud.component.ts +++ b/client/src/app/components/ps-product-crud/ps-product-crud.component.ts @@ -11,10 +11,12 @@ 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} from '@angular/forms'; +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'; @@ -27,14 +29,15 @@ import {ProductDialogData, PsProductDialogComponent} from '../ps-product-dialog/ templateUrl: './ps-product-crud.component.html', styleUrls: ['./ps-product-crud.component.css'], imports: [ - CommonModule, ReactiveFormsModule, + CommonModule, ReactiveFormsModule, FormsModule, MatTable, MatColumnDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatNoDataRow, MatSortModule, MatPaginatorModule, MatFormField, MatLabel, MatInput, MatButton, MatIconButton, MatIcon, MatDialogModule, - MatProgressSpinnerModule + MatProgressSpinnerModule, + MatCheckboxModule ] }) export class PsProductCrudComponent implements OnInit { @@ -50,12 +53,16 @@ export class PsProductCrudComponent implements OnInit { private manMap = new Map(); private supMap = new Map(); - displayed: string[] = ['id', 'name', 'category', 'manufacturer', 'supplier', 'priceTtc', 'quantity', 'actions']; + // 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; @@ -121,6 +128,8 @@ export class PsProductCrudComponent implements OnInit { 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) ?? '') : '', @@ -203,4 +212,55 @@ export class PsProductCrudComponent implements OnInit { } }); } + + // --- 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))); + } + }); + } } diff --git a/docker-compose.yml.OLD b/docker-compose.yml.OLD deleted file mode 100644 index 17b893c..0000000 --- a/docker-compose.yml.OLD +++ /dev/null @@ -1,85 +0,0 @@ -services: - mysql: - image: mysql:8.4 - container_name: gameovergne-mysql - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: gameovergne_app - MYSQL_USER: gameovergne - MYSQL_PASSWORD: gameovergne - volumes: - - ./mysql-data:/var/lib/mysql - ports: - - "3366:3306" - networks: - - gameovergne - restart: unless-stopped - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-proot"] - interval: 10s - timeout: 5s - retries: 10 - - spring: - image: registry.vincent-guillet.fr/gameovergne-api:dev-latest - container_name: gameovergne-api - depends_on: - mysql: - condition: service_healthy - environment: - SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/gameovergne_app?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC - SPRING_DATASOURCE_USERNAME: gameovergne - SPRING_DATASOURCE_PASSWORD: gameovergne - PRESTASHOP_API_KEY: 2AQPG13MJ8X117U6FJ5NGHPS93HE34AB - SERVER_PORT: 3000 - networks: - - traefik - - gameovergne - restart: unless-stopped - labels: - - traefik.enable=true - - traefik.docker.network=traefik - - - traefik.http.routers.gameovergne-api.rule=Host(`dev.vincent-guillet.fr`) && PathPrefix(`/gameovergne-api`) - - traefik.http.routers.gameovergne-api.entrypoints=edge - - traefik.http.routers.gameovergne-api.service=gameovergne-api - - traefik.http.services.gameovergne-api.loadbalancer.server.port=3000 - - traefik.http.routers.gameovergne-api.middlewares=gameovergne-api-stripprefix - - traefik.http.middlewares.gameovergne-api-stripprefix.stripprefix.prefixes=/gameovergne-api - - angular: - image: registry.vincent-guillet.fr/gameovergne-client:dev-latest - container_name: gameovergne-client - depends_on: - - spring - networks: - - gameovergne - - traefik - restart: unless-stopped - labels: - - traefik.enable=true - - traefik.docker.network=traefik - - - traefik.http.routers.gameovergne-client.rule=Host(`dev.vincent-guillet.fr`) && (Path(`/gameovergne`) || PathPrefix(`/gameovergne/`)) - - traefik.http.routers.gameovergne-client.entrypoints=edge - - traefik.http.routers.gameovergne-client.service=gameovergne-client - - traefik.http.routers.gameovergne-client.middlewares=gameovergne-slash,gameovergne-client-stripprefix - - - traefik.http.middlewares.gameovergne-slash.redirectregex.regex=^https?://([^/]+)/gameovergne$$ - - traefik.http.middlewares.gameovergne-slash.redirectregex.replacement=https://$${1}/gameovergne/ - - traefik.http.middlewares.gameovergne-slash.redirectregex.permanent=true - - - traefik.http.middlewares.gameovergne-client-stripprefix.stripprefix.prefixes=/gameovergne - - - traefik.http.services.gameovergne-client.loadbalancer.server.port=80 - - - traefik.http.routers.gameovergne-ps.rule=Host(`dev.vincent-guillet.fr`) && PathPrefix(`/gameovergne/ps`) - - traefik.http.routers.gameovergne-ps.entrypoints=edge - - traefik.http.routers.gameovergne-ps.service=gameovergne-client - - traefik.http.routers.gameovergne-ps.middlewares=gameovergne-client-stripprefix - -networks: - traefik: - external: true - gameovergne: - external: true \ No newline at end of file
+ + + + + + ID {{ el.id }}