dev #2

Merged
vincentguillet merged 2 commits from dev into main 2025-12-23 15:27:46 +00:00
3 changed files with 100 additions and 94 deletions

View File

@@ -7,6 +7,16 @@
<mat-icon>add</mat-icon>&nbsp;Nouveau produit <mat-icon>add</mat-icon>&nbsp;Nouveau produit
</button> </button>
@if (selection.hasValue()) {
<button
mat-raised-button
color="warn"
(click)="deleteSelected()"
[disabled]="isLoading">
<mat-icon>delete</mat-icon>&nbsp;Supprimer la sélection
</button>
}
<mat-form-field appearance="outline" class="filter"> <mat-form-field appearance="outline" class="filter">
<mat-label>Filtrer</mat-label> <mat-label>Filtrer</mat-label>
<input matInput <input matInput
@@ -15,15 +25,36 @@
[disabled]="isLoading"> [disabled]="isLoading">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mat-elevation-z2 product-list-root"> <div class="mat-elevation-z2 product-list-root">
<!-- Overlay de chargement --> @if (isLoading) {
<div class="product-list-loading-overlay" *ngIf="isLoading"> <div class="product-list-loading-overlay">
<mat-spinner diameter="48"></mat-spinner> <mat-spinner diameter="48"></mat-spinner>
</div> </div>
}
<table mat-table [dataSource]="dataSource" matSort> <table mat-table [dataSource]="dataSource" matSort>
<!-- select column -->
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox
[checked]="isAllSelected()"
[indeterminate]="isAnySelected() && !isAllSelected()"
(change)="masterToggle($event.checked)"
[disabled]="isLoading">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let el">
<mat-checkbox
[checked]="selection.isSelected(el)"
(change)="toggleSelection(el, $event.checked)"
[disabled]="isLoading">
</mat-checkbox>
</td>
</ng-container>
<!-- existing columns follow (id, name, ...) -->
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>ID</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>ID</th>
<td mat-cell *matCellDef="let el">{{ el.id }}</td> <td mat-cell *matCellDef="let el">{{ el.id }}</td>

View File

@@ -11,10 +11,12 @@ import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input'; import {MatInput} from '@angular/material/input';
import {MatButton, MatIconButton} from '@angular/material/button'; import {MatButton, MatIconButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon'; 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 {MatDialog, MatDialogModule} from '@angular/material/dialog';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {forkJoin, finalize} from 'rxjs'; import {forkJoin, finalize} from 'rxjs';
import {SelectionModel} from '@angular/cdk/collections';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {PsItem} from '../../interfaces/ps-item'; import {PsItem} from '../../interfaces/ps-item';
import {ProductListItem} from '../../interfaces/product-list-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', templateUrl: './ps-product-crud.component.html',
styleUrls: ['./ps-product-crud.component.css'], styleUrls: ['./ps-product-crud.component.css'],
imports: [ imports: [
CommonModule, ReactiveFormsModule, CommonModule, ReactiveFormsModule, FormsModule,
MatTable, MatColumnDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatTable, MatColumnDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatNoDataRow, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatNoDataRow,
MatSortModule, MatPaginatorModule, MatSortModule, MatPaginatorModule,
MatFormField, MatLabel, MatInput, MatFormField, MatLabel, MatInput,
MatButton, MatIconButton, MatIcon, MatButton, MatIconButton, MatIcon,
MatDialogModule, MatDialogModule,
MatProgressSpinnerModule MatProgressSpinnerModule,
MatCheckboxModule
] ]
}) })
export class PsProductCrudComponent implements OnInit { export class PsProductCrudComponent implements OnInit {
@@ -50,12 +53,16 @@ export class PsProductCrudComponent implements OnInit {
private manMap = new Map<number, string>(); private manMap = new Map<number, string>();
private supMap = new Map<number, string>(); private supMap = new Map<number, string>();
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<any>([]); dataSource = new MatTableDataSource<any>([]);
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<any>; @ViewChild(MatTable) table!: MatTable<any>;
// selection model (multiple)
selection = new SelectionModel<any>(true, []);
filterCtrl = this.fb.control<string>(''); filterCtrl = this.fb.control<string>('');
isLoading = false; isLoading = false;
@@ -121,6 +128,8 @@ export class PsProductCrudComponent implements OnInit {
private bindProducts(p: (ProductListItem & { priceHt?: number })[]) { private bindProducts(p: (ProductListItem & { priceHt?: number })[]) {
const vat = 0.2; const vat = 0.2;
// clear selection because objects will be new after reload
this.selection.clear();
this.dataSource.data = p.map(x => ({ this.dataSource.data = p.map(x => ({
...x, ...x,
categoryName: x.id_category_default ? (this.catMap.get(x.id_category_default) ?? '') : '', 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)));
}
});
}
} }

View File

@@ -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