Compare commits
3 Commits
00208f08c9
...
6ae3e64ee9
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ae3e64ee9 | |||
|
|
e0beed6c6e | ||
|
|
1a111b420d |
@@ -7,6 +7,16 @@
|
|||||||
<mat-icon>add</mat-icon> Nouveau produit
|
<mat-icon>add</mat-icon> Nouveau produit
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@if (selection.hasValue()) {
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="warn"
|
||||||
|
(click)="deleteSelected()"
|
||||||
|
[disabled]="isLoading">
|
||||||
|
<mat-icon>delete</mat-icon> 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>
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user