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) {
+
+
+
+ }
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
ID |
{{ el.id }} |
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