refactor: rename components and update dialog implementations; add confirm dialog for deletion actions

This commit is contained in:
Vincent Guillet
2025-11-04 18:13:37 +01:00
parent 2fe52830d8
commit 3bc2a27d76
57 changed files with 396 additions and 1109 deletions

View File

@@ -1,6 +0,0 @@
<app-generic-list
[service]="brandService"
[fields]="fields"
title="Marques"
idKey="id">
</app-generic-list>

View File

@@ -1,6 +0,0 @@
<app-generic-list
[service]="categoryService"
[fields]="fields"
title="Catégories"
idKey="id">
</app-generic-list>

View File

@@ -0,0 +1,8 @@
<h2 mat-dialog-title>{{ data.title ?? 'Confirmation' }}</h2>
<mat-dialog-content>
<p>{{ data.message ?? 'Êtes-vous sûr·e ?' }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" (click)="close(false)">Annuler</button>
<button mat-flat-button color="warn" type="button" (click)="close(true)">Supprimer</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,21 @@
import { Component, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-confirm-dialog',
standalone: true,
imports: [CommonModule, MatDialogModule, MatButtonModule],
templateUrl: './confirm-dialog.component.html',
})
export class ConfirmDialogComponent {
constructor(
private readonly dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { title?: string; message?: string }
) {}
close(result: boolean) {
this.dialogRef.close(result);
}
}

View File

@@ -13,7 +13,7 @@
<mat-label>{{ f.label }}</mat-label>
<mat-select [formControlName]="f.key">
@let opts = (f.options$ | async) ?? f.options ?? [];
@let opts = (f.options ?? (f.options$ | async) ?? []);
@for (opt of opts; track $index) {
<mat-option [value]="f.valueKey ? opt?.[f.valueKey] : opt">

View File

@@ -36,12 +36,12 @@ type Field = {
})
export class GenericDialogComponent implements OnInit {
form!: FormGroup;
fields: Field[] = [];
fields?: Field[];
constructor(
private readonly fb: FormBuilder,
private readonly dialogRef: MatDialogRef<GenericDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { item?: any; fields?: Field[]; title?: string }
@Inject(MAT_DIALOG_DATA) public data?: { item?: any; fields?: Field[]; title?: string }
) {
}

View File

@@ -0,0 +1,9 @@
<app-generic-list
[service]="brandService"
[fields]="fields"
title="Gestion des marques"
addTitle="Ajouter une marque"
editTitle="Modifier la marque"
deleteTitle="Supprimer la marque"
idKey="id">
</app-generic-list>

View File

@@ -1,7 +1,7 @@
import {
Component, inject
} from '@angular/core';
import {BrandService} from '../../services/app/brand.service';
import {BrandService} from '../../../services/app/brand.service';
import {GenericListComponent} from '../generic-list/generic-list.component';
@Component({

View File

@@ -0,0 +1,9 @@
<app-generic-list
[service]="categoryService"
[fields]="fields"
title="Gestin des catégories"
addTitle="Ajouter une catégorie"
editTitle="Modifier la catégorie"
deleteTitle="Supprimer la catégorie"
idKey="id">
</app-generic-list>

View File

@@ -2,7 +2,7 @@ import {
Component, inject
} from '@angular/core';
import {GenericListComponent} from '../generic-list/generic-list.component';
import {CategoryService} from '../../services/app/category.service';
import {CategoryService} from '../../../services/app/category.service';
@Component({
selector: 'app-category-list',

View File

@@ -15,7 +15,7 @@
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
border-bottom: 1px solid rgba(0,0,0,.08);
border-bottom: 1px solid rgba(0, 0, 0, .08);
padding-bottom: .75rem;
}
@@ -27,12 +27,11 @@
/* ===== Cartes (filtre, tableau, pagination) partagent le même style ===== */
.gl-block {
border: 1px solid rgba(0,0,0,.08);
border: 1px solid rgba(0, 0, 0, .08);
border-radius: 12px;
background: var(--gl-surface, #fff);
box-shadow:
0 1px 2px rgba(0,0,0,.04),
0 2px 8px rgba(0,0,0,.06);
box-shadow: 0 1px 2px rgba(0, 0, 0, .04),
0 2px 8px rgba(0, 0, 0, .06);
}
/* ===== Barre de filtre ===== */
@@ -66,7 +65,7 @@
top: 0;
z-index: 2;
background: inherit;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.08);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .08);
}
/* Cellules */
@@ -80,8 +79,13 @@
}
/* Zebra + hover */
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] { background: rgba(0,0,0,.015); }
.gl-table tr.mat-mdc-row:hover td[mat-cell] { background: rgba(0,0,0,.035); }
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] {
background: rgba(0, 0, 0, .015);
}
.gl-table tr.mat-mdc-row:hover td[mat-cell] {
background: rgba(0, 0, 0, .035);
}
/* Actions */
.actions-cell {
@@ -90,42 +94,78 @@
justify-content: center;
gap: .4rem;
}
.actions-cell .mat-mdc-icon-button { width: 40px; height: 40px; }
.actions-cell .mat-mdc-icon-button {
width: 40px;
height: 40px;
}
/* ===== Pagination ===== */
.gl-paginator-wrap {
padding: .25rem .5rem;
}
.gl-paginator {
margin-top: .25rem;
padding-top: .5rem;
border-top: 1px solid rgba(0,0,0,.08);
border-top: 1px solid rgba(0, 0, 0, .08);
display: flex;
justify-content: flex-end;
}
/* ===== Responsive ===== */
@media (max-width: 799px) {
.generic-list { padding: 0.75rem 1rem; }
.gl-header { flex-direction: column; align-items: stretch; gap: 0.75rem; }
.gl-table { min-width: 0; }
.generic-list {
padding: 0.75rem 1rem;
}
.gl-header {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.gl-table {
min-width: 0;
}
.gl-table th[mat-header-cell],
.gl-table td[mat-cell] { white-space: normal; padding: 10px 12px; }
.actions-cell { justify-content: flex-start; }
.gl-table td[mat-cell] {
white-space: normal;
padding: 10px 12px;
}
.actions-cell {
justify-content: flex-start;
}
}
/* ===== Dark mode ===== */
@media (prefers-color-scheme: dark) {
.gl-block {
background: #1b1b1b;
border-color: rgba(255,255,255,.08);
box-shadow:
0 1px 2px rgba(0,0,0,.6),
0 2px 8px rgba(0,0,0,.45);
border-color: rgba(255, 255, 255, .08);
box-shadow: 0 1px 2px rgba(0, 0, 0, .6),
0 2px 8px rgba(0, 0, 0, .45);
}
.gl-header {
border-bottom-color: rgba(255, 255, 255, .08);
}
.gl-table th[mat-header-cell] {
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, .08);
}
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] {
background: rgba(255, 255, 255, .025);
}
.gl-table tr.mat-mdc-row:hover td[mat-cell] {
background: rgba(255, 255, 255, .06);
}
.gl-paginator {
border-top-color: rgba(255, 255, 255, .08);
}
.gl-header { border-bottom-color: rgba(255,255,255,.08); }
.gl-table th[mat-header-cell] { box-shadow: inset 0 -1px 0 rgba(255,255,255,.08); }
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] { background: rgba(255,255,255,.025); }
.gl-table tr.mat-mdc-row:hover td[mat-cell] { background: rgba(255,255,255,.06); }
.gl-paginator { border-top-color: rgba(255,255,255,.08); }
}

View File

@@ -4,17 +4,16 @@
<div class="gl-controls">
<button mat-flat-button color="primary" type="button" (click)="openDialog(null)">
Ajouter
{{ addTitle ?? 'Ajouter' }}
</button>
</div>
</div>
<div class="gl-filter-bar gl-block">
<mat-form-field class="gl-filter" appearance="outline">
<mat-label>Filtrer</mat-label>
<mat-label>Rechercher</mat-label>
<input
matInput
placeholder="Tapez pour filtrer…"
(input)="applyFilter($any($event.target).value)"
aria-label="Filtrer le tableau"
/>
@@ -57,6 +56,7 @@
aria-label="Modifier"
>
<mat-icon>edit</mat-icon>
</button>
<button
mat-icon-button

View File

@@ -5,10 +5,12 @@ import {MatSort, MatSortModule} from '@angular/material/sort';
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
import {MatButtonModule} from '@angular/material/button';
import {CommonModule} from '@angular/common';
import {CrudService} from '../../services/crud.service';
import {GenericDialogComponent} from '../generic-dialog/generic-dialog.component';
import {CrudService} from '../../../services/crud.service';
import {GenericDialogComponent} from '../../dialog/generic-dialog/generic-dialog.component';
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {MatIcon} from '@angular/material/icon';
import {ConfirmDialogComponent} from '../../dialog/confirm-dialog/confirm-dialog.component';
import {MatChip} from '@angular/material/chips';
type Field = {
key: string;
@@ -16,7 +18,6 @@ type Field = {
sortable?: boolean;
displayKey?: string;
displayFn?: (value: any, element?: any) => string;
// nouveau : clé de tri (peut être chemin 'brand.name') ou fonction personnalisée
sortKey?: string | ((item: any) => any);
sortFn?: (a: any, b: any) => number;
};
@@ -24,14 +25,17 @@ type Field = {
@Component({
selector: 'app-generic-list',
standalone: true,
imports: [CommonModule, MatTableModule, MatPaginatorModule, MatSortModule, MatDialogModule, MatButtonModule, MatInput, MatLabel, MatFormField, MatIcon],
imports: [CommonModule, MatTableModule, MatPaginatorModule, MatSortModule, MatDialogModule, MatButtonModule, MatInput, MatLabel, MatFormField, MatIcon, MatChip],
templateUrl: './generic-list.component.html',
styleUrl: './generic-list.component.css'
})
export class GenericListComponent<T> implements OnInit, AfterViewInit {
@Input() service!: CrudService<T>;
@Input() fields: Field[] = [];
@Input() fields?: Field[];
@Input() title = '';
@Input() addTitle?: string;
@Input() editTitle?: string;
@Input() deleteTitle?: string;
@Input() idKey = 'id';
@Input() dialogComponent: any = GenericDialogComponent;
@Output() add = new EventEmitter<T>();
@@ -48,32 +52,30 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
}
ngOnInit() {
this.fields = this.fields ?? [];
this.displayedColumns = this.fields.map(f => f.key).concat(['actions']);
this.load();
}
ngAfterViewInit() {
// configure le sortingDataAccessor avant d'attacher le sort
this.dataSource.sortingDataAccessor = (data: any, sortHeaderId: string) => {
// trouver le Field correspondant (ou utiliser la clé brute)
const field = this.fields.find(f => f.key === sortHeaderId);
const field = this.fields!.find(f => f.key === sortHeaderId);
if (!field) {
const raw = data?.[sortHeaderId];
const raw = getByPath(data, sortHeaderId) ?? data?.[sortHeaderId];
return raw == null ? '' : String(raw);
}
// priorité : sortFn sur le field (géré par sort comparator plus bas)
if (field.sortKey) {
if (typeof field.sortKey === 'function') {
const v = field.sortKey(data);
return v == null ? '' : String(v);
}
const v = getByPath(data, field.sortKey);
const v = getByPath(data, field.sortKey as string);
return v == null ? '' : String(v);
}
// fallback : valeur simple ou displayKey si object
const val = data?.[field.key];
const val = getByPath(data, field.key);
if (val == null) return '';
if (typeof val === 'object') {
if (field.displayKey && val[field.displayKey] != null) return String(val[field.displayKey]);
@@ -90,14 +92,13 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
return String(val);
};
// attacher le MatSort
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
// si un Field a sortFn définie, adapter le comparator global pour l'utiliser
const originalSortData = this.dataSource.sortData;
this.dataSource.sortData = (data: T[], sort: MatSort) => {
if (!sort || !sort.active || sort.direction === '') return originalSortData.call(this.dataSource, data, sort);
const field = this.fields.find(f => f.key === sort.active);
const field = this.fields!.find(f => f.key === sort.active);
if (field?.sortFn) {
const dir = sort.direction === 'asc' ? 1 : -1;
return [...data].sort((a, b) => dir * field.sortFn!(a, b));
@@ -106,12 +107,11 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
};
}
// DEBUG + normalisation de l'id pour éviter 'undefined' lors de update
load() {
this.service.getAll().subscribe(items => {
console.debug('Loaded items from service:', items);
this.dataSource.data = (items as any[]).map(item => {
const normalizedId = item?.[this.idKey] ?? item?.id ?? item?._id ?? item?.platformId ?? null;
const normalizedId = getByPath(item, this.idKey) ?? item?.[this.idKey] ?? item?.id ?? item?._id ?? item?.platformId ?? null;
return {...item, [this.idKey]: normalizedId};
}) as T[];
});
@@ -122,14 +122,18 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
}
openDialog(item: any | null) {
const originalId = item ? (item[this.idKey] ?? item?.id ?? item?._id) : null;
const originalId = item ? (getByPath(item, this.idKey) ?? item?.[this.idKey] ?? item?.id ?? item?._id) : null;
const dialogTitle = item
? (this.editTitle ?? 'Modifier')
: (this.addTitle ?? 'Ajouter');
const dialogRef = this.dialog.open(this.dialogComponent, {
width: '420px',
data: {
item: item ? {...item} : {},
fields: this.fields,
title: item ? 'Modifier' : 'Ajouter',
title: dialogTitle,
originalId
}
});
@@ -138,7 +142,7 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
if (!result) return;
if (item) {
const idToUpdate = originalId ?? result?.[this.idKey] ?? result?.id ?? result?._id;
const idToUpdate = originalId ?? getByPath(result, this.idKey) ?? result?.[this.idKey] ?? result?.id ?? result?._id;
if (idToUpdate == null) {
console.error('Cannot update: id is null/undefined for item', {item, result});
return;
@@ -157,19 +161,31 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
}
remove(item: any) {
const id = item[this.idKey] ?? item?.id ?? item?._id;
const id = getByPath(item, this.idKey) ?? item[this.idKey] ?? item?.id ?? item?._id;
if (id == null) {
console.error('Cannot delete: id is null/undefined for item', item);
return;
}
this.service.delete(id).subscribe(() => {
this.delete.emit(item);
this.load();
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '380px',
data: {
title: this.deleteTitle ?? 'Confirmer la suppression',
message: 'Voulez-vous vraiment supprimer cet élément ? Cette action est irréversible.'
}
});
dialogRef.afterClosed().subscribe((confirmed: boolean) => {
if (!confirmed) return;
this.service.delete(id).subscribe(() => {
this.delete.emit(item);
this.load();
});
});
}
displayValue(element: any, field: Field): string {
const val = element?.[field.key];
const val = getByPath(element, field.key);
if (field.displayFn) {
try {
return String(field.displayFn(val, element) ?? '');
@@ -193,15 +209,11 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
return String(val);
}
trackByField(_index: number, field: Field) {
return field?.key ?? _index;
}
protected readonly HTMLInputElement = HTMLInputElement;
}
/** Helpers */
function getByPath(obj: any, path: string): any {
function getByPath(obj: any, path: string | undefined): any {
if (!obj || !path) return undefined;
if (typeof path !== 'string') return undefined;
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
}

View File

@@ -0,0 +1,9 @@
<app-generic-list
[service]="platformService"
[fields]="fields"
title="Gestion des plateformes"
addTitle="Ajouter une plateforme"
editTitle="Modifier la plateforme"
deleteTitle="Supprimer la plateforme"
idKey="id">
</app-generic-list>

View File

@@ -2,9 +2,9 @@ import {
Component,
inject
} from '@angular/core';
import {PlatformService} from '../../services/app/platform.service';
import {PlatformService} from '../../../services/app/platform.service';
import {GenericListComponent} from '../generic-list/generic-list.component';
import {BrandService} from '../../services/app/brand.service';
import {BrandService} from '../../../services/app/brand.service';
@Component({
selector: 'app-platform-list',
@@ -26,11 +26,10 @@ export class PlatformListComponent {
key: 'brand',
label: 'Marque',
type: 'select',
options$: this.brandService.getAll(), // transmet les brands dynamiquement
displayKey: 'name', // affiche brand.name dans le select
// valueKey: 'id' // uncommenter si backend attend l'id au lieu de l'objet
options$: this.brandService.getAll(),
displayKey: 'name',
sortable: true,
sortKey: 'brand.name' // permet de trier par brand.name
sortKey: 'brand.name'
}
];
}

View File

@@ -0,0 +1,9 @@
<app-generic-list
[service]="productService"
[fields]="fields"
title="Liste des produits"
addTitle="Ajouter un produit"
editTitle="Modifier le produit"
deleteTitle="Supprimer le produit"
idKey="id">
</app-generic-list>

View File

@@ -0,0 +1,110 @@
import {
Component,
inject
} from '@angular/core';
import {GenericListComponent} from '../generic-list/generic-list.component';
import {ProductService} from '../../../services/app/product.service';
import {BrandService} from '../../../services/app/brand.service';
import {PlatformService} from '../../../services/app/platform.service';
import {CategoryService} from '../../../services/app/category.service';
import {ConditionService} from '../../../services/app/condition.service';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
standalone: true,
imports: [
GenericListComponent
],
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent {
productService: ProductService = inject(ProductService);
categoryService: CategoryService = inject(CategoryService);
brandService: BrandService = inject(BrandService);
platformService: PlatformService = inject(PlatformService);
conditionService: ConditionService = inject(ConditionService);
fields = [
{key: 'title', label: 'Titre', sortable: true},
{
key: 'description',
label: 'Description',
},
{
key: 'category',
label: 'Catégorie',
type: 'select',
options$: this.categoryService.getAll(),
displayKey: 'name',
sortable: true,
sortKey: 'category.name'
},
{
key: 'platform.brand',
label: 'Marque',
type: 'select',
options$: this.brandService.getAll(),
displayKey: 'name',
sortable: true,
sortKey: 'platform.brand.name'
},
{
key: 'platform',
label: 'Plateforme',
type: 'select',
options$: this.platformService.getAll(),
displayKey: 'name',
sortable: true,
sortKey: 'platform.name'
},
{
key: 'condition.displayName',
label: 'État',
type: 'select',
options$: this.conditionService.getAll(),
displayKey: 'displayName',
sortable: true,
sortKey: 'condition.displayName'
},
{
key: 'complete',
label: 'Complet',
type: 'checkbox',
sortable: true,
sortKey: 'complete',
displayFn: (value: boolean): string => value ? '✔ Oui' : '✗ Non'
},
{
key: 'manualIncluded',
label: 'Notice',
type: 'checkbox',
sortable: true,
sortKey: 'manualIncluded',
displayFn: (value: boolean): string => value ? '✔ Oui' : '✗ Non'
},
{
key: 'price',
label: 'Prix',
sortable: true,
sortKey: 'price',
displayFn: (value: string | number): string => {
if (value == null || value === '') return '';
try {
return new Intl
.NumberFormat('fr-FR', {style: 'currency', currency: 'EUR', maximumFractionDigits: 2})
.format(Number(value));
} catch {
return String(value);
}
}
},
{
key: 'quantity',
label: 'Quantité',
sortable: true,
sortKey: 'quantity'
}
];
}

View File

@@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {PlatformListComponent} from '../platform-list/platform-list.component';
import {CategoryListComponent} from '../category-list/category-list.component';
import {BrandListComponent} from '../brand-list/brand-list.component';
import {PlatformListComponent} from '../../list/platform-list/platform-list.component';
import {CategoryListComponent} from '../../list/category-list/category-list.component';
import {BrandListComponent} from '../../list/brand-list/brand-list.component';
@Component({
selector: 'app-admin-navbar',

View File

@@ -19,7 +19,7 @@
<mat-icon>person</mat-icon>
Profil
</button>
<button mat-menu-item (click)="authService.logout().subscribe()">
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
Se déconnecter
</button>

View File

@@ -2,12 +2,12 @@ import {Component, inject} from '@angular/core';
import {MatToolbar} from '@angular/material/toolbar';
import {MatButton} from '@angular/material/button';
import {Router, RouterLink} from '@angular/router';
import {AuthService} from '../../services/auth/auth.service';
import {AuthService} from '../../../services/auth/auth.service';
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
import {MatIcon} from '@angular/material/icon';
@Component({
selector: 'app-navbar',
selector: 'app-main-navbar',
standalone: true,
imports: [
MatToolbar,
@@ -18,22 +18,18 @@ import {MatIcon} from '@angular/material/icon';
MatMenu,
MatMenuItem
],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.css'
templateUrl: './main-navbar.component.html',
styleUrl: './main-navbar.component.css'
})
export class NavbarComponent {
export class MainNavbarComponent {
protected readonly authService = inject(AuthService);
private readonly router: Router = inject(Router);
login() {
this.router.navigate(['/login'], {queryParams: {redirect: '/profile'}}).then();
}
logout() {
this.authService.logout().subscribe({
next: () => {
this.login();
this.router.navigate(['/login'], {queryParams: {redirect: '/profile'}}).then();
},
error: (err) => {
console.error('Logout failed:', err);

View File

@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NavbarComponent } from './navbar.component';
describe('NavbarComponent', () => {
let component: NavbarComponent;
let fixture: ComponentFixture<NavbarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NavbarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NavbarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,6 +0,0 @@
<app-generic-list
[service]="platformService"
[fields]="fields"
title="Plateformes"
idKey="id">
</app-generic-list>

View File

@@ -1 +0,0 @@
<p>product-form works!</p>

View File

@@ -1,12 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-product-form',
standalone: true,
imports: [],
templateUrl: './product-form.component.html',
styleUrl: './product-form.component.css'
})
export class ProductFormComponent {
}

View File

@@ -1,125 +0,0 @@
<div class="generic-list">
<!-- Header (bouton à droite) -->
<div class="gl-header">
<div class="gl-controls">
<button mat-flat-button color="accent" (click)="onAdd()">
<mat-icon>add</mat-icon>&nbsp;Ajouter
</button>
</div>
</div>
<!-- Barre de recherche sous le header -->
<div class="gl-filter-bar gl-block">
<mat-form-field class="gl-filter" appearance="outline">
<mat-label>Rechercher</mat-label>
<input
matInput
placeholder="Tapez pour filtrer…"
(input)="applyFilter($any($event.target).value)"
aria-label="Filtrer le tableau"
/>
</mat-form-field>
</div>
<!-- Tableau -->
<div class="gl-table-wrapper gl-block">
<table mat-table [dataSource]="dataSource" class="gl-table" matSort>
<!-- Title Column -->
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Nom</th>
<td mat-cell *matCellDef="let product">{{ product.title }}</td>
</ng-container>
<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
<td mat-cell *matCellDef="let product">{{ product.description }}</td>
</ng-container>
<!-- Category Column -->
<ng-container matColumnDef="category">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Catégorie</th>
<td mat-cell *matCellDef="let product">{{ product.category.name }}</td>
</ng-container>
<!-- Platform Column -->
<ng-container matColumnDef="platform">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Plateforme</th>
<td mat-cell *matCellDef="let product">{{ product.platform.name }}</td>
</ng-container>
<!-- Condition Column -->
<ng-container matColumnDef="condition">
<th mat-header-cell *matHeaderCellDef mat-sort-header>État</th>
<td mat-cell *matCellDef="let product">{{ product.condition.displayName }}</td>
</ng-container>
<!-- Complete Column -->
<ng-container matColumnDef="complete">
<th mat-header-cell *matHeaderCellDef>Complet</th>
<td mat-cell *matCellDef="let product">
@if (product.complete) {
<mat-icon color="primary">check_circle</mat-icon>
} @else {
<mat-icon color="warn">cancel</mat-icon>
}
</td>
</ng-container>
<!-- Manual Column -->
<ng-container matColumnDef="manual">
<th mat-header-cell *matHeaderCellDef>Notice</th>
<td mat-cell *matCellDef="let product">
@if (product.manual) {
<mat-icon color="primary">check_circle</mat-icon>
} @else {
<mat-icon color="warn">cancel</mat-icon>
}
</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Prix</th>
<td mat-cell *matCellDef="let product">{{ product.price | currency:'EUR' }}</td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Quantité</th>
<td mat-cell *matCellDef="let product">{{ product.quantity }}</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="actions-head">Actions</th>
<td mat-cell *matCellDef="let product" class="actions-cell">
<button mat-icon-button (click)="onEdit(product)" aria-label="Modifier">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDelete(product)" aria-label="Supprimer">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<!-- Pagination avec même carte -->
<div class="gl-paginator-wrap gl-block">
<mat-paginator
class="gl-paginator"
[pageSize]="10"
[pageSizeOptions]="[5,10,25]"
showFirstLastButtons>
</mat-paginator>
</div>
@if (!products || products.length === 0) {
<div class="no-products gl-block">Aucun produit trouvé.</div>
}
</div>

View File

@@ -1,118 +0,0 @@
import {
Component,
Input,
Output,
EventEmitter,
ViewChild,
AfterViewInit,
OnChanges,
SimpleChanges,
OnInit,
inject
} from '@angular/core';
import {
MatCell, MatCellDef, MatColumnDef, MatHeaderCell,
MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatTable,
MatTableDataSource
} from '@angular/material/table';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {Product} from '../../interfaces/product';
import {ProductService} from '../../services/app/product.service';
import {MatDialog} from '@angular/material/dialog';
import {MatButton, MatIconButton} from '@angular/material/button';
import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatIcon} from '@angular/material/icon';
import {MatInput} from '@angular/material/input';
import {CurrencyPipe} from '@angular/common';
@Component({
selector: 'app-products-list',
templateUrl: './products-list.component.html',
standalone: true,
imports: [
MatButton,
MatCell,
MatCellDef,
MatColumnDef,
MatFormField,
MatHeaderCell,
MatHeaderRow,
MatHeaderRowDef,
MatIcon,
MatIconButton,
MatInput,
MatPaginator,
MatRow,
MatRowDef,
MatSort,
MatTable,
MatHeaderCellDef,
CurrencyPipe,
MatLabel
],
styleUrls: ['./products-list.component.css']
})
export class ProductsListComponent implements OnInit, AfterViewInit, OnChanges {
@Input() products: Product[] = [];
@Output() add = new EventEmitter<Product>();
@Output() edit = new EventEmitter<Product>();
@Output() delete = new EventEmitter<Product>();
displayedColumns: string[] = ['title', 'description', 'category', 'platform', 'condition', 'complete', 'manual', 'price', 'quantity', 'actions'];
dataSource = new MatTableDataSource<Product>([]);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
private readonly productService: ProductService = inject(ProductService);
private readonly dialog = inject(MatDialog);
ngOnInit(): void {
if (!this.products || this.products.length === 0) {
this.loadProducts();
} else {
this.dataSource.data = this.products;
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['products']) {
this.dataSource.data = this.products || [];
}
}
ngAfterViewInit(): void {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadProducts() {
this.productService.getProducts().subscribe({
next: (products: Product[]) => {
this.products = products || []
this.dataSource.data = this.products;
console.log("Fetched products:", this.products);
},
error: () => this.products = []
});
}
onAdd(): void {
}
onEdit(product: Product): void {
}
onDelete(product: Product): void {
this.delete.emit(product);
this.productService.deleteProduct((product as any).id).subscribe(() => this.loadProducts());
}
applyFilter(value: string): void {
this.dataSource.filter = (value || '').trim().toLowerCase();
}
}