refactor: rename components and update admin navbar; implement generic list and dialog components for brands, categories, and platforms
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
<mat-tab-group>
|
<mat-tab-group>
|
||||||
<mat-tab label="Marques">
|
<mat-tab label="Marques">
|
||||||
<app-brands-list></app-brands-list>
|
<app-brand-list></app-brand-list>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab label="Plateformes">
|
<mat-tab label="Plateformes">
|
||||||
<app-platforms-list></app-platforms-list>
|
<app-platform-list></app-platform-list>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab label="Catégories">
|
<mat-tab label="Catégories">
|
||||||
<app-categories-list></app-categories-list>
|
<app-category-list></app-category-list>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {MatAnchor, MatButton} from "@angular/material/button";
|
|
||||||
import {MatIcon} from '@angular/material/icon';
|
|
||||||
import {RouterLink, RouterLinkActive} from '@angular/router';
|
|
||||||
import {MatTab, MatTabGroup} from '@angular/material/tabs';
|
import {MatTab, MatTabGroup} from '@angular/material/tabs';
|
||||||
import {BrandsListComponent} from '../brands-list/brands-list.component';
|
import {PlatformListComponent} from '../platform-list/platform-list.component';
|
||||||
import {PlatformsListComponent} from '../platforms-list/platforms-list.component';
|
import {CategoryListComponent} from '../category-list/category-list.component';
|
||||||
import {CategoriesListComponent} from '../categories-list/categories-list.component';
|
import {BrandListComponent} from '../brand-list/brand-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-navbar',
|
selector: 'app-admin-navbar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
MatButton,
|
|
||||||
MatIcon,
|
|
||||||
RouterLinkActive,
|
|
||||||
RouterLink,
|
|
||||||
MatAnchor,
|
|
||||||
MatTabGroup,
|
MatTabGroup,
|
||||||
MatTab,
|
MatTab,
|
||||||
BrandsListComponent,
|
CategoryListComponent,
|
||||||
PlatformsListComponent,
|
BrandListComponent,
|
||||||
CategoriesListComponent
|
PlatformListComponent
|
||||||
],
|
],
|
||||||
templateUrl: './admin-navbar.component.html',
|
templateUrl: './admin-navbar.component.html',
|
||||||
styleUrl: './admin-navbar.component.css'
|
styleUrl: './admin-navbar.component.css'
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<h2 mat-dialog-title>{{ brandExists ? 'Modifier la marque' : 'Nouvelle marque' }}</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<mat-form-field appearance="fill" style="width:100%;">
|
|
||||||
<mat-label>Nom</mat-label>
|
|
||||||
<input matInput [(ngModel)]="brand.name" />
|
|
||||||
</mat-form-field>
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
|
||||||
<button mat-button (click)="cancel()">Annuler</button>
|
|
||||||
<button mat-flat-button color="primary" (click)="save()">Enregistrer</button>
|
|
||||||
</mat-dialog-actions>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
MatDialogRef,
|
|
||||||
MAT_DIALOG_DATA,
|
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
|
||||||
MatDialogActions
|
|
||||||
} from '@angular/material/dialog';
|
|
||||||
import { Brand } from '../../interfaces/brand';
|
|
||||||
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {FormsModule} from '@angular/forms';
|
|
||||||
import {MatButton} from '@angular/material/button';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-brand-dialog',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
|
||||||
MatFormField,
|
|
||||||
MatLabel,
|
|
||||||
MatInput,
|
|
||||||
FormsModule,
|
|
||||||
MatDialogActions,
|
|
||||||
MatButton
|
|
||||||
],
|
|
||||||
templateUrl: './brand-dialog.component.html'
|
|
||||||
})
|
|
||||||
export class BrandDialogComponent {
|
|
||||||
brand: Brand = { id: '', name: '' }
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly dialogRef: MatDialogRef<BrandDialogComponent>,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { brand: Brand }
|
|
||||||
) {
|
|
||||||
this.brand = { ...(data?.brand || { id: '', name: '' }) };
|
|
||||||
}
|
|
||||||
|
|
||||||
get brandExists(): boolean {
|
|
||||||
return !!this.data?.brand?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.dialogRef.close(this.brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="brandService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Marques"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
23
client/src/app/components/brand-list/brand-list.component.ts
Normal file
23
client/src/app/components/brand-list/brand-list.component.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Component, inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import {BrandService} from '../../services/app/brand.service';
|
||||||
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-brand-list',
|
||||||
|
templateUrl: './brand-list.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
GenericListComponent
|
||||||
|
],
|
||||||
|
styleUrls: ['./brand-list.component.css']
|
||||||
|
})
|
||||||
|
export class BrandListComponent {
|
||||||
|
|
||||||
|
brandService: BrandService = inject(BrandService)
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
{key: 'name', label: 'Nom', sortable: true}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<div class="container" style="padding:16px;">
|
|
||||||
<div class="toolbar">
|
|
||||||
<button mat-flat-button color="accent" (click)="onAdd()">
|
|
||||||
<mat-icon>add</mat-icon> Ajouter
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<mat-form-field class="filter">
|
|
||||||
<input matInput placeholder="Rechercher" (input)="applyFilter($any($event.target).value)">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1" matSort>
|
|
||||||
|
|
||||||
<!-- Name Column -->
|
|
||||||
<ng-container matColumnDef="name">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
|
||||||
<td mat-cell *matCellDef="let brand">{{ brand.name }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Actions Column -->
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
|
||||||
<td mat-cell *matCellDef="let brand" class="actions-cell">
|
|
||||||
<button mat-icon-button (click)="onEdit(brand)">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button color="warn" (click)="onDelete(brand)">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5,10,25]" showFirstLastButtons></mat-paginator>
|
|
||||||
|
|
||||||
@if (!brands || brands.length === 0) {
|
|
||||||
<div class="no-brands">
|
|
||||||
Aucune plateforme trouvée.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@@ -1,139 +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 {Brand} from '../../interfaces/brand';
|
|
||||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
|
||||||
import {MatIcon} from '@angular/material/icon';
|
|
||||||
import {MatFormField} from '@angular/material/form-field';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {BrandService} from '../../services/app/brand.service';
|
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
|
||||||
import { BrandDialogComponent } from '../brand-dialog/brand-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-brands-list',
|
|
||||||
templateUrl: './brands-list.component.html',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatButton,
|
|
||||||
MatIcon,
|
|
||||||
MatFormField,
|
|
||||||
MatInput,
|
|
||||||
MatTable,
|
|
||||||
MatColumnDef,
|
|
||||||
MatHeaderCell,
|
|
||||||
MatCell,
|
|
||||||
MatHeaderCellDef,
|
|
||||||
MatCellDef,
|
|
||||||
MatSort,
|
|
||||||
MatIconButton,
|
|
||||||
MatHeaderRow,
|
|
||||||
MatRow,
|
|
||||||
MatHeaderRowDef,
|
|
||||||
MatRowDef,
|
|
||||||
MatPaginator
|
|
||||||
],
|
|
||||||
styleUrls: ['./brands-list.component.css']
|
|
||||||
})
|
|
||||||
export class BrandsListComponent implements OnInit, AfterViewInit, OnChanges {
|
|
||||||
|
|
||||||
@Input() brands: Brand[] = [];
|
|
||||||
@Output() add = new EventEmitter<Brand>();
|
|
||||||
@Output() edit = new EventEmitter<Brand>();
|
|
||||||
@Output() delete = new EventEmitter<Brand>();
|
|
||||||
|
|
||||||
displayedColumns: string[] = ['name', 'actions'];
|
|
||||||
dataSource = new MatTableDataSource<Brand>([]);
|
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
||||||
@ViewChild(MatSort) sort!: MatSort;
|
|
||||||
|
|
||||||
private readonly brandService: BrandService = inject(BrandService);
|
|
||||||
private readonly dialog = inject(MatDialog);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (!this.brands || this.brands.length === 0) {
|
|
||||||
this.loadBrands();
|
|
||||||
} else {
|
|
||||||
this.dataSource.data = this.brands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (changes['brands']) {
|
|
||||||
this.dataSource.data = this.brands || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.dataSource.paginator = this.paginator;
|
|
||||||
this.dataSource.sort = this.sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBrands() {
|
|
||||||
this.brandService.getBrands().subscribe({
|
|
||||||
next: (brands:Brand[]) => {
|
|
||||||
this.brands = brands || []
|
|
||||||
this.dataSource.data = this.brands;
|
|
||||||
console.log("Fetched brands:", this.brands);
|
|
||||||
},
|
|
||||||
error: () => this.brands = []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(): void {
|
|
||||||
const ref = this.dialog.open(BrandDialogComponent, {
|
|
||||||
data: { brand: { id: '', name: '', brand: undefined } },
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Brand) => {
|
|
||||||
if (result) {
|
|
||||||
this.add.emit(result);
|
|
||||||
this.brandService.addBrand(result).subscribe(() => this.loadBrands());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEdit(brand: Brand): void {
|
|
||||||
const ref = this.dialog.open(BrandDialogComponent, {
|
|
||||||
data: { brand: { ...brand } },
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Brand) => {
|
|
||||||
if (result) {
|
|
||||||
this.edit.emit(result);
|
|
||||||
this.brandService.updateBrand((brand as any).id, result).subscribe(() => this.loadBrands());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete(brand: Brand): void {
|
|
||||||
this.delete.emit(brand);
|
|
||||||
this.brandService.deleteBrand((brand as any).id).subscribe(() => this.loadBrands());
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter(value: string): void {
|
|
||||||
this.dataSource.filter = (value || '').trim().toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
max-width: 240px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th {
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.mat-icon-button {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-brands {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 16px;
|
|
||||||
color: rgba(0,0,0,0.6);
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<div class="container" style="padding:16px;">
|
|
||||||
<div class="toolbar">
|
|
||||||
<button mat-flat-button color="accent" (click)="onAdd()">
|
|
||||||
<mat-icon>add</mat-icon> Ajouter
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<mat-form-field class="filter">
|
|
||||||
<input matInput placeholder="Rechercher" (input)="applyFilter($any($event.target).value)">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1" matSort>
|
|
||||||
|
|
||||||
<!-- Name Column -->
|
|
||||||
<ng-container matColumnDef="name">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
|
||||||
<td mat-cell *matCellDef="let category">{{ category.name }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Actions Column -->
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
|
||||||
<td mat-cell *matCellDef="let category" class="actions-cell">
|
|
||||||
<button mat-icon-button (click)="onEdit(category)">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button color="warn" (click)="onDelete(category)">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5,10,25]" showFirstLastButtons></mat-paginator>
|
|
||||||
|
|
||||||
@if (!categories || categories.length === 0) {
|
|
||||||
<div class="no-categories">
|
|
||||||
Aucune catégorie trouvée.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@@ -1,138 +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 {Category} from '../../interfaces/category';
|
|
||||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
|
||||||
import {MatIcon} from '@angular/material/icon';
|
|
||||||
import {MatFormField} from '@angular/material/form-field';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {CategoryService} from '../../services/app/category.service';
|
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
|
||||||
import { CategoryDialogComponent } from '../category-dialog/category-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-categories-list',
|
|
||||||
templateUrl: './categories-list.component.html',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatButton,
|
|
||||||
MatIcon,
|
|
||||||
MatFormField,
|
|
||||||
MatInput,
|
|
||||||
MatTable,
|
|
||||||
MatColumnDef,
|
|
||||||
MatHeaderCell,
|
|
||||||
MatCell,
|
|
||||||
MatHeaderCellDef,
|
|
||||||
MatCellDef,
|
|
||||||
MatSort,
|
|
||||||
MatIconButton,
|
|
||||||
MatHeaderRow,
|
|
||||||
MatRow,
|
|
||||||
MatHeaderRowDef,
|
|
||||||
MatRowDef,
|
|
||||||
MatPaginator
|
|
||||||
],
|
|
||||||
styleUrls: ['./categories-list.component.css']
|
|
||||||
})
|
|
||||||
export class CategoriesListComponent implements OnInit, AfterViewInit, OnChanges {
|
|
||||||
|
|
||||||
@Input() categories: Category[] = [];
|
|
||||||
@Output() add = new EventEmitter<Category>();
|
|
||||||
@Output() edit = new EventEmitter<Category>();
|
|
||||||
@Output() delete = new EventEmitter<Category>();
|
|
||||||
|
|
||||||
displayedColumns: string[] = ['name', 'actions'];
|
|
||||||
dataSource = new MatTableDataSource<Category>([]);
|
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
||||||
@ViewChild(MatSort) sort!: MatSort;
|
|
||||||
|
|
||||||
private readonly categoryService: CategoryService = inject(CategoryService);
|
|
||||||
private readonly dialog = inject(MatDialog);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (!this.categories || this.categories.length === 0) {
|
|
||||||
this.loadCategories();
|
|
||||||
} else {
|
|
||||||
this.dataSource.data = this.categories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (changes['categories']) {
|
|
||||||
this.dataSource.data = this.categories || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.dataSource.paginator = this.paginator;
|
|
||||||
this.dataSource.sort = this.sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCategories() {
|
|
||||||
this.categoryService.getCategories().subscribe({
|
|
||||||
next: (categories:Category[]) => {
|
|
||||||
this.categories = categories || []
|
|
||||||
this.dataSource.data = this.categories;
|
|
||||||
},
|
|
||||||
error: () => this.categories = []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(): void {
|
|
||||||
const ref = this.dialog.open(CategoryDialogComponent, {
|
|
||||||
data: { category: { id: '', name: '' } },
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Category) => {
|
|
||||||
if (result) {
|
|
||||||
this.add.emit(result);
|
|
||||||
this.categoryService.addCategory(result).subscribe(() => this.loadCategories());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEdit(category: Category): void {
|
|
||||||
const ref = this.dialog.open(CategoryDialogComponent, {
|
|
||||||
data: { category: { ...category } },
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Category) => {
|
|
||||||
if (result) {
|
|
||||||
this.edit.emit(result);
|
|
||||||
this.categoryService.updateCategory((category as any).id, result).subscribe(() => this.loadCategories());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete(category: Category): void {
|
|
||||||
this.delete.emit(category);
|
|
||||||
this.categoryService.deleteCategory((category as any).id).subscribe(() => this.loadCategories());
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter(value: string): void {
|
|
||||||
this.dataSource.filter = (value || '').trim().toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<h2 mat-dialog-title>{{ categoryExists ? 'Modifier la catégorie' : 'Nouvelle catégorie' }}</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<mat-form-field appearance="fill" style="width:100%;">
|
|
||||||
<mat-label>Nom</mat-label>
|
|
||||||
<input matInput [(ngModel)]="category.name" />
|
|
||||||
</mat-form-field>
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
|
||||||
<button mat-button (click)="cancel()">Annuler</button>
|
|
||||||
<button mat-flat-button color="primary" (click)="save()">Enregistrer</button>
|
|
||||||
</mat-dialog-actions>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
MatDialogRef,
|
|
||||||
MAT_DIALOG_DATA,
|
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
|
||||||
MatDialogActions
|
|
||||||
} from '@angular/material/dialog';
|
|
||||||
import { Category } from '../../interfaces/category';
|
|
||||||
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {FormsModule} from '@angular/forms';
|
|
||||||
import {MatButton} from '@angular/material/button';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-category-dialog',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
|
||||||
MatFormField,
|
|
||||||
MatLabel,
|
|
||||||
MatInput,
|
|
||||||
FormsModule,
|
|
||||||
MatDialogActions,
|
|
||||||
MatButton
|
|
||||||
],
|
|
||||||
templateUrl: './category-dialog.component.html'
|
|
||||||
})
|
|
||||||
export class CategoryDialogComponent {
|
|
||||||
category: Category = { id: '', name: '' }
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly dialogRef: MatDialogRef<CategoryDialogComponent>,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { category: Category }
|
|
||||||
) {
|
|
||||||
this.category = { ...(data?.category || { id: '', name: '' }) };
|
|
||||||
}
|
|
||||||
|
|
||||||
get categoryExists(): boolean {
|
|
||||||
return !!this.data?.category?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.dialogRef.close(this.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="categoryService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Catégories"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Component, inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
|
import {CategoryService} from '../../services/app/category.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-category-list',
|
||||||
|
templateUrl: './category-list.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
GenericListComponent
|
||||||
|
],
|
||||||
|
styleUrls: ['./category.component.css']
|
||||||
|
})
|
||||||
|
export class CategoryListComponent {
|
||||||
|
|
||||||
|
categoryService: CategoryService = inject(CategoryService)
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
{key: 'name', label: 'Nom', sortable: true}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<form [formGroup]="form" (ngSubmit)="save()">
|
||||||
|
<h2 mat-dialog-title>{{ data?.title ?? 'Edit' }}</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
@for (f of (fields ?? []); track $index) {
|
||||||
|
|
||||||
|
@if (f.type === 'checkbox') {
|
||||||
|
<mat-checkbox [formControlName]="f.key">
|
||||||
|
{{ f.label }}
|
||||||
|
</mat-checkbox>
|
||||||
|
} @else if (f.type === 'select') {
|
||||||
|
<mat-form-field style="width:100%; margin-top:8px;">
|
||||||
|
<mat-label>{{ f.label }}</mat-label>
|
||||||
|
|
||||||
|
<mat-select [formControlName]="f.key">
|
||||||
|
@let opts = (f.options$ | async) ?? f.options ?? [];
|
||||||
|
|
||||||
|
@for (opt of opts; track $index) {
|
||||||
|
<mat-option [value]="f.valueKey ? opt?.[f.valueKey] : opt">
|
||||||
|
{{ f.displayKey ? opt?.[f.displayKey] : (opt?.name ?? opt?.label ?? opt) }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
} @else {
|
||||||
|
<mat-form-field style="width:100%; margin-top:8px;">
|
||||||
|
<mat-label>{{ f.label }}</mat-label>
|
||||||
|
|
||||||
|
@if (f.type === 'textarea') {
|
||||||
|
<textarea matInput [formControlName]="f.key"></textarea>
|
||||||
|
} @else {
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[type]="f.type ?? 'text'"
|
||||||
|
[formControlName]="f.key"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-button type="button" (click)="close()">Annuler</button>
|
||||||
|
<button mat-flat-button color="primary" type="submit">Enregistrer</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {MatDialogRef, MAT_DIALOG_DATA, MatDialogModule} from '@angular/material/dialog';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
type Field = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type?: 'text' | 'number' | 'textarea' | 'checkbox' | 'select';
|
||||||
|
options?: any[]; // options statiques
|
||||||
|
options$?: Observable<any[]>; // options dynamiques
|
||||||
|
displayKey?: string; // clé à afficher (ex: 'name')
|
||||||
|
valueKey?: string; // clé valeur (ex: 'id'), si absente la valeur entière de l'objet est utilisée
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-generic-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatSelectModule
|
||||||
|
],
|
||||||
|
templateUrl: './generic-dialog.component.html'
|
||||||
|
})
|
||||||
|
export class GenericDialogComponent implements OnInit {
|
||||||
|
form!: FormGroup;
|
||||||
|
fields: Field[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly fb: FormBuilder,
|
||||||
|
private readonly dialogRef: MatDialogRef<GenericDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { item?: any; fields?: Field[]; title?: string }
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.fields = this.data?.fields ?? [];
|
||||||
|
this.form = this.fb.group({});
|
||||||
|
for (const f of this.fields) {
|
||||||
|
const initial =
|
||||||
|
this.data?.item?.[f.key] ??
|
||||||
|
(f.type === 'checkbox' ? false : (f.type === 'select' ? null : ''));
|
||||||
|
this.form.addControl(f.key, this.fb.control(initial));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.dialogRef.close({...this.data?.item, ...this.form.value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/* ===== Container centré ===== */
|
||||||
|
.generic-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem clamp(1rem, 3vw, 3rem);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Header ===== */
|
||||||
|
.gl-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||||
|
padding-bottom: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.1rem, 1.3rem + 0.3vw, 1.6rem);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Cartes (filtre, tableau, pagination) partagent le même style ===== */
|
||||||
|
.gl-block {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Barre de filtre ===== */
|
||||||
|
.gl-filter-bar {
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-filter {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Tableau ===== */
|
||||||
|
.gl-table-wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header sticky */
|
||||||
|
.gl-table th[mat-header-cell] {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: inherit;
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cellules */
|
||||||
|
.gl-table th[mat-header-cell],
|
||||||
|
.gl-table td[mat-cell] {
|
||||||
|
padding: 14px 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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); }
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.actions-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
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; }
|
||||||
|
.gl-table th[mat-header-cell],
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<div class="generic-list">
|
||||||
|
<div class="gl-header">
|
||||||
|
<h3 class="gl-title">{{ title }}</h3>
|
||||||
|
|
||||||
|
<div class="gl-controls">
|
||||||
|
<button mat-flat-button color="primary" type="button" (click)="openDialog(null)">
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gl-filter-bar gl-block">
|
||||||
|
<mat-form-field class="gl-filter" appearance="outline">
|
||||||
|
<mat-label>Filtrer</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
placeholder="Tapez pour filtrer…"
|
||||||
|
(input)="applyFilter($any($event.target).value)"
|
||||||
|
aria-label="Filtrer le tableau"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gl-table-wrapper gl-block">
|
||||||
|
<table
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
matSort
|
||||||
|
matSortDisableClear
|
||||||
|
class="gl-table"
|
||||||
|
>
|
||||||
|
@for (col of (fields ?? []); track $index) {
|
||||||
|
<ng-container [matColumnDef]="col.key">
|
||||||
|
<th
|
||||||
|
mat-header-cell
|
||||||
|
*matHeaderCellDef
|
||||||
|
mat-sort-header
|
||||||
|
[disabled]="!col.sortable"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
{{ displayValue(element, col) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="actions-cell">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="openDialog(element)"
|
||||||
|
aria-label="Modifier"
|
||||||
|
>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
color="warn"
|
||||||
|
type="button"
|
||||||
|
(click)="remove(element)"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div class="gl-paginator-wrap gl-block">
|
||||||
|
<mat-paginator
|
||||||
|
class="gl-paginator"
|
||||||
|
[pageSize]="10"
|
||||||
|
[pageSizeOptions]="[5, 10, 25, 50]"
|
||||||
|
showFirstLastButtons
|
||||||
|
></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
207
client/src/app/components/generic-list/generic-list.component.ts
Normal file
207
client/src/app/components/generic-list/generic-list.component.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import {Component, Input, Output, EventEmitter, ViewChild, AfterViewInit, OnInit} from '@angular/core';
|
||||||
|
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
|
||||||
|
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
|
||||||
|
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 {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
|
||||||
|
type Field = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-generic-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatTableModule, MatPaginatorModule, MatSortModule, MatDialogModule, MatButtonModule, MatInput, MatLabel, MatFormField, MatIcon],
|
||||||
|
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() title = '';
|
||||||
|
@Input() idKey = 'id';
|
||||||
|
@Input() dialogComponent: any = GenericDialogComponent;
|
||||||
|
@Output() add = new EventEmitter<T>();
|
||||||
|
@Output() edit = new EventEmitter<T>();
|
||||||
|
@Output() delete = new EventEmitter<T>();
|
||||||
|
|
||||||
|
dataSource = new MatTableDataSource<T>([]);
|
||||||
|
displayedColumns: string[] = [];
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort!: MatSort;
|
||||||
|
|
||||||
|
constructor(private readonly dialog: MatDialog) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
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);
|
||||||
|
if (!field) {
|
||||||
|
const raw = 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);
|
||||||
|
return v == null ? '' : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback : valeur simple ou displayKey si object
|
||||||
|
const val = data?.[field.key];
|
||||||
|
if (val == null) return '';
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
if (field.displayKey && val[field.displayKey] != null) return String(val[field.displayKey]);
|
||||||
|
const candidates = ['name', 'title', 'label', 'id'];
|
||||||
|
for (const k of candidates) {
|
||||||
|
if (val[k] != null) return String(val[k]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(val);
|
||||||
|
} catch {
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
// attacher le MatSort
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
if (field?.sortFn) {
|
||||||
|
const dir = sort.direction === 'asc' ? 1 : -1;
|
||||||
|
return [...data].sort((a, b) => dir * field.sortFn!(a, b));
|
||||||
|
}
|
||||||
|
return originalSortData.call(this.dataSource, data, sort);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
return {...item, [this.idKey]: normalizedId};
|
||||||
|
}) as T[];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(value: string) {
|
||||||
|
this.dataSource.filter = (value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog(item: any | null) {
|
||||||
|
const originalId = item ? (item[this.idKey] ?? item?.id ?? item?._id) : null;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(this.dialogComponent, {
|
||||||
|
width: '420px',
|
||||||
|
data: {
|
||||||
|
item: item ? {...item} : {},
|
||||||
|
fields: this.fields,
|
||||||
|
title: item ? 'Modifier' : 'Ajouter',
|
||||||
|
originalId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((result: any) => {
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const idToUpdate = originalId ?? result?.[this.idKey] ?? result?.id ?? result?._id;
|
||||||
|
if (idToUpdate == null) {
|
||||||
|
console.error('Cannot update: id is null/undefined for item', {item, result});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.service.update(idToUpdate, result).subscribe(() => {
|
||||||
|
this.edit.emit(result);
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.service.add(result).subscribe(() => {
|
||||||
|
this.add.emit(result);
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(item: any) {
|
||||||
|
const id = 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displayValue(element: any, field: Field): string {
|
||||||
|
const val = element?.[field.key];
|
||||||
|
if (field.displayFn) {
|
||||||
|
try {
|
||||||
|
return String(field.displayFn(val, element) ?? '');
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (val == null) return '';
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
if (field.displayKey && val[field.displayKey] != null) return String(val[field.displayKey]);
|
||||||
|
const candidates = ['name', 'title', 'label', 'id'];
|
||||||
|
for (const k of candidates) {
|
||||||
|
if (val[k] != null) return String(val[k]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(val);
|
||||||
|
} catch {
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByField(_index: number, field: Field) {
|
||||||
|
return field?.key ?? _index;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly HTMLInputElement = HTMLInputElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helpers */
|
||||||
|
function getByPath(obj: any, path: string): any {
|
||||||
|
if (!obj || !path) return undefined;
|
||||||
|
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<h2 mat-dialog-title>{{ platformExists ? 'Modifier la plateforme' : 'Nouvelle plateforme' }}</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<mat-form-field appearance="fill" style="width:100%;">
|
|
||||||
<mat-label>Nom</mat-label>
|
|
||||||
<input matInput [(ngModel)]="platform.name" />
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field appearance="fill" style="width:100%;">
|
|
||||||
<mat-label>Marque</mat-label>
|
|
||||||
<mat-select [(ngModel)]="platform.brand" disableRipple>
|
|
||||||
@for (brand of brands; track brand.id) {
|
|
||||||
<mat-option [value]="brand">{{ brand.name }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
|
||||||
<button mat-button (click)="cancel()">Annuler</button>
|
|
||||||
<button mat-flat-button color="primary" (click)="save()">Enregistrer</button>
|
|
||||||
</mat-dialog-actions>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import {Component, inject, Inject, OnInit} from '@angular/core';
|
|
||||||
import {MatButton} from "@angular/material/button";
|
|
||||||
import {
|
|
||||||
MAT_DIALOG_DATA,
|
|
||||||
MatDialogActions,
|
|
||||||
MatDialogContent,
|
|
||||||
MatDialogRef,
|
|
||||||
MatDialogTitle
|
|
||||||
} from "@angular/material/dialog";
|
|
||||||
import {MatFormField, MatLabel} from "@angular/material/form-field";
|
|
||||||
import {MatInput} from "@angular/material/input";
|
|
||||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
|
||||||
import {Brand} from '../../interfaces/brand';
|
|
||||||
import {Platform} from '../../interfaces/platform';
|
|
||||||
import {MatOption} from '@angular/material/core';
|
|
||||||
import {MatSelect} from '@angular/material/select';
|
|
||||||
import {BrandService} from '../../services/app/brand.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-platform-dialog',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatButton,
|
|
||||||
MatDialogActions,
|
|
||||||
MatDialogContent,
|
|
||||||
MatDialogTitle,
|
|
||||||
MatFormField,
|
|
||||||
MatInput,
|
|
||||||
MatLabel,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FormsModule,
|
|
||||||
MatOption,
|
|
||||||
MatSelect
|
|
||||||
],
|
|
||||||
templateUrl: './platform-dialog.component.html',
|
|
||||||
styleUrl: './platform-dialog.component.css'
|
|
||||||
})
|
|
||||||
export class PlatformDialogComponent implements OnInit {
|
|
||||||
|
|
||||||
private readonly brandService: BrandService = inject(BrandService);
|
|
||||||
|
|
||||||
platform: Platform = { id: '', name: '', brand: undefined };
|
|
||||||
brands: Brand[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly dialogRef: MatDialogRef<PlatformDialogComponent>,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { platform: Platform }
|
|
||||||
) {
|
|
||||||
this.platform = { ...data.platform };
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.loadBrands();
|
|
||||||
}
|
|
||||||
|
|
||||||
get platformExists(): boolean {
|
|
||||||
return !!this.data?.platform?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBrands() {
|
|
||||||
this.brandService.getBrands().subscribe({
|
|
||||||
next: (brands:Brand[]) => this.brands = brands || [],
|
|
||||||
error: () => this.brands = []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.dialogRef.close(this.platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="platformService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Plateformes"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import {PlatformService} from '../../services/app/platform.service';
|
||||||
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
|
import {BrandService} from '../../services/app/brand.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-platform-list',
|
||||||
|
templateUrl: './platform-list.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
GenericListComponent
|
||||||
|
],
|
||||||
|
styleUrls: ['./platform-list.component.css']
|
||||||
|
})
|
||||||
|
export class PlatformListComponent {
|
||||||
|
|
||||||
|
platformService: PlatformService = inject(PlatformService)
|
||||||
|
brandService: BrandService = inject(BrandService);
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
{key: 'name', label: 'Nom', sortable: true},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'brand.name' // permet de trier par brand.name
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
max-width: 240px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th {
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.mat-icon-button {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-platforms {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 16px;
|
|
||||||
color: rgba(0,0,0,0.6);
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<div class="container" style="padding:16px;">
|
|
||||||
<div class="toolbar">
|
|
||||||
<button mat-flat-button color="accent" (click)="onAdd()">
|
|
||||||
<mat-icon>add</mat-icon> Ajouter
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<mat-form-field class="filter">
|
|
||||||
<input matInput placeholder="Rechercher" (input)="applyFilter($any($event.target).value)">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1" matSort>
|
|
||||||
|
|
||||||
<!-- Name Column -->
|
|
||||||
<ng-container matColumnDef="name">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
|
||||||
<td mat-cell *matCellDef="let platform">{{ platform.name }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Brand Column -->
|
|
||||||
<ng-container matColumnDef="brand">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Marque</th>
|
|
||||||
<td mat-cell *matCellDef="let platform">{{ platform.brand.name }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Actions Column -->
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
|
||||||
<td mat-cell *matCellDef="let platform" class="actions-cell">
|
|
||||||
<button mat-icon-button (click)="onEdit(platform)">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button color="warn" (click)="onDelete(platform)">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5,10,25]" showFirstLastButtons></mat-paginator>
|
|
||||||
|
|
||||||
@if (!platforms || platforms.length === 0) {
|
|
||||||
<div class="no-platforms">
|
|
||||||
Aucune plateforme trouvée.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@@ -1,139 +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 {Platform} from '../../interfaces/platform';
|
|
||||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
|
||||||
import {MatIcon} from '@angular/material/icon';
|
|
||||||
import {MatFormField} from '@angular/material/form-field';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {PlatformService} from '../../services/app/platform.service';
|
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
|
||||||
import { PlatformDialogComponent } from '../platform-dialog/platform-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-platforms-list',
|
|
||||||
templateUrl: './platforms-list.component.html',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatButton,
|
|
||||||
MatIcon,
|
|
||||||
MatFormField,
|
|
||||||
MatInput,
|
|
||||||
MatTable,
|
|
||||||
MatColumnDef,
|
|
||||||
MatHeaderCell,
|
|
||||||
MatCell,
|
|
||||||
MatHeaderCellDef,
|
|
||||||
MatCellDef,
|
|
||||||
MatSort,
|
|
||||||
MatIconButton,
|
|
||||||
MatHeaderRow,
|
|
||||||
MatRow,
|
|
||||||
MatHeaderRowDef,
|
|
||||||
MatRowDef,
|
|
||||||
MatPaginator
|
|
||||||
],
|
|
||||||
styleUrls: ['./platforms-list.component.css']
|
|
||||||
})
|
|
||||||
export class PlatformsListComponent implements OnInit, AfterViewInit, OnChanges {
|
|
||||||
|
|
||||||
@Input() platforms: Platform[] = [];
|
|
||||||
@Output() add = new EventEmitter<Platform>();
|
|
||||||
@Output() edit = new EventEmitter<Platform>();
|
|
||||||
@Output() delete = new EventEmitter<Platform>();
|
|
||||||
|
|
||||||
displayedColumns: string[] = ['name', 'brand', 'actions'];
|
|
||||||
dataSource = new MatTableDataSource<Platform>([]);
|
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
||||||
@ViewChild(MatSort) sort!: MatSort;
|
|
||||||
|
|
||||||
private readonly platformService: PlatformService = inject(PlatformService);
|
|
||||||
private readonly dialog = inject(MatDialog);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (!this.platforms || this.platforms.length === 0) {
|
|
||||||
this.loadPlatforms();
|
|
||||||
} else {
|
|
||||||
this.dataSource.data = this.platforms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (changes['platforms']) {
|
|
||||||
this.dataSource.data = this.platforms || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.dataSource.paginator = this.paginator;
|
|
||||||
this.dataSource.sort = this.sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPlatforms() {
|
|
||||||
this.platformService.getPlatforms().subscribe({
|
|
||||||
next: (platforms:Platform[]) => {
|
|
||||||
this.platforms = platforms || []
|
|
||||||
this.dataSource.data = this.platforms;
|
|
||||||
console.log("Fetched platforms:", this.platforms);
|
|
||||||
},
|
|
||||||
error: () => this.platforms = []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(): void {
|
|
||||||
const ref = this.dialog.open(PlatformDialogComponent, {
|
|
||||||
data: { platform: { id: '', name: '', brand: undefined } },
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Platform) => {
|
|
||||||
if (result) {
|
|
||||||
this.add.emit(result);
|
|
||||||
this.platformService.addPlatform(result).subscribe(() => this.loadPlatforms());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEdit(platform: Platform): void {
|
|
||||||
const ref = this.dialog.open(PlatformDialogComponent, {
|
|
||||||
data: { platform: { ...platform } },
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Platform) => {
|
|
||||||
if (result) {
|
|
||||||
this.edit.emit(result);
|
|
||||||
this.platformService.updatePlatform((platform as any).id, result).subscribe(() => this.loadPlatforms());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete(platform: Platform): void {
|
|
||||||
this.delete.emit(platform);
|
|
||||||
this.platformService.deletePlatform((platform as any).id).subscribe(() => this.loadPlatforms());
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter(value: string): void {
|
|
||||||
this.dataSource.filter = (value || '').trim().toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<h2 mat-dialog-title>{{ productExists ? 'Modifier la plateforme' : 'Nouvelle plateforme' }}</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<mat-form-field appearance="fill" style="width:100%;">
|
|
||||||
<mat-label>Nom</mat-label>
|
|
||||||
<input matInput [(ngModel)]="product.title" />
|
|
||||||
</mat-form-field>
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
|
||||||
<button mat-button (click)="cancel()">Annuler</button>
|
|
||||||
<button mat-flat-button color="primary" (click)="save()">Enregistrer</button>
|
|
||||||
</mat-dialog-actions>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {Component, Inject} from '@angular/core';
|
|
||||||
import {
|
|
||||||
MatDialogRef,
|
|
||||||
MAT_DIALOG_DATA,
|
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
|
||||||
MatDialogActions
|
|
||||||
} from '@angular/material/dialog';
|
|
||||||
import {Product} from '../../interfaces/product';
|
|
||||||
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {FormsModule} from '@angular/forms';
|
|
||||||
import {MatButton} from '@angular/material/button';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-product-dialog',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
|
||||||
MatFormField,
|
|
||||||
MatLabel,
|
|
||||||
MatInput,
|
|
||||||
FormsModule,
|
|
||||||
MatDialogActions,
|
|
||||||
MatButton
|
|
||||||
],
|
|
||||||
templateUrl: './product-dialog.component.html'
|
|
||||||
})
|
|
||||||
export class ProductDialogComponent {
|
|
||||||
product: Product = {
|
|
||||||
id: '',
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
price: 0,
|
|
||||||
quantity: 0,
|
|
||||||
complete: false,
|
|
||||||
manualIncluded: false,
|
|
||||||
category: undefined,
|
|
||||||
platform: undefined,
|
|
||||||
condition: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly dialogRef: MatDialogRef<ProductDialogComponent>,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { product: Product }
|
|
||||||
) {
|
|
||||||
this.product = {...(data?.product || {id: '', name: ''})};
|
|
||||||
}
|
|
||||||
|
|
||||||
get productExists(): boolean {
|
|
||||||
return !!this.data?.product?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.dialogRef.close(this.product);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* ===== Container centré ===== */
|
||||||
|
.generic-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem clamp(1rem, 3vw, 3rem);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Header ===== */
|
||||||
|
.gl-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end; /* bouton à droite */
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||||
|
padding-bottom: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Cartes (filtre, table, pagination) ===== */
|
||||||
|
.gl-block {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Barre de filtre ===== */
|
||||||
|
.gl-filter-bar { padding: .75rem; }
|
||||||
|
.gl-filter { display: block; width: 100%; max-width: none; }
|
||||||
|
|
||||||
|
/* ===== Tableau ===== */
|
||||||
|
.gl-table-wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0.25rem 0.5rem; /* espace interne pour éviter l'effet "collé" */
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
min-width: 720px; /* permet le scroll horizontal si trop de colonnes */
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table th[mat-header-cell] {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: inherit;
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table th[mat-header-cell],
|
||||||
|
.gl-table td[mat-cell] {
|
||||||
|
padding: 14px 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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); }
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.actions-head { width: 1%; white-space: nowrap; }
|
||||||
|
.actions-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Empty state ===== */
|
||||||
|
.no-products {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(0,0,0,.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Responsive ===== */
|
||||||
|
@media (max-width: 799px) {
|
||||||
|
.generic-list { padding: 0.75rem 1rem; }
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 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);
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
.no-products { color: rgba(255,255,255,.7); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,104 +1,125 @@
|
|||||||
<div class="container" style="padding:16px;">
|
<div class="generic-list">
|
||||||
<div class="toolbar">
|
<!-- Header (bouton à droite) -->
|
||||||
<button mat-flat-button color="accent" (click)="onAdd()">
|
<div class="gl-header">
|
||||||
<mat-icon>add</mat-icon> Ajouter
|
<div class="gl-controls">
|
||||||
</button>
|
<button mat-flat-button color="accent" (click)="onAdd()">
|
||||||
|
<mat-icon>add</mat-icon> Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<mat-form-field class="filter">
|
<!-- Barre de recherche sous le header -->
|
||||||
<input matInput placeholder="Rechercher" (input)="applyFilter($any($event.target).value)">
|
<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>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1" matSort>
|
<!-- Tableau -->
|
||||||
|
<div class="gl-table-wrapper gl-block">
|
||||||
|
<table mat-table [dataSource]="dataSource" class="gl-table" matSort>
|
||||||
|
|
||||||
<!-- Title Column -->
|
<!-- Title Column -->
|
||||||
<ng-container matColumnDef="title">
|
<ng-container matColumnDef="title">
|
||||||
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Nom</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.title }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.title }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Description Column -->
|
<!-- Description Column -->
|
||||||
<ng-container matColumnDef="description">
|
<ng-container matColumnDef="description">
|
||||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.description }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.description }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Category Column -->
|
<!-- Category Column -->
|
||||||
<ng-container matColumnDef="category">
|
<ng-container matColumnDef="category">
|
||||||
<th mat-header-cell *matHeaderCellDef>Catégorie</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Catégorie</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.category.name }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.category.name }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Platform Column -->
|
<!-- Platform Column -->
|
||||||
<ng-container matColumnDef="platform">
|
<ng-container matColumnDef="platform">
|
||||||
<th mat-header-cell *matHeaderCellDef>Plateforme</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Plateforme</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.platform.name }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.platform.name }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Condition Column -->
|
<!-- Condition Column -->
|
||||||
<ng-container matColumnDef="condition">
|
<ng-container matColumnDef="condition">
|
||||||
<th mat-header-cell *matHeaderCellDef>État</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>État</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.condition.displayName }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.condition.displayName }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Complete Column -->
|
<!-- Complete Column -->
|
||||||
<ng-container matColumnDef="complete">
|
<ng-container matColumnDef="complete">
|
||||||
<th mat-header-cell *matHeaderCellDef>Complet</th>
|
<th mat-header-cell *matHeaderCellDef>Complet</th>
|
||||||
<td mat-cell *matCellDef="let product">
|
<td mat-cell *matCellDef="let product">
|
||||||
@if (product.complete) {
|
@if (product.complete) {
|
||||||
<mat-icon color="primary">check_circle</mat-icon>
|
<mat-icon color="primary">check_circle</mat-icon>
|
||||||
} @else {
|
} @else {
|
||||||
<mat-icon color="warn">cancel</mat-icon>
|
<mat-icon color="warn">cancel</mat-icon>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Manual Column -->
|
<!-- Manual Column -->
|
||||||
<ng-container matColumnDef="manual">
|
<ng-container matColumnDef="manual">
|
||||||
<th mat-header-cell *matHeaderCellDef>Notice</th>
|
<th mat-header-cell *matHeaderCellDef>Notice</th>
|
||||||
<td mat-cell *matCellDef="let product">
|
<td mat-cell *matCellDef="let product">
|
||||||
@if (product.manual) {
|
@if (product.manual) {
|
||||||
<mat-icon color="primary">check_circle</mat-icon>
|
<mat-icon color="primary">check_circle</mat-icon>
|
||||||
} @else {
|
} @else {
|
||||||
<mat-icon color="warn">cancel</mat-icon>
|
<mat-icon color="warn">cancel</mat-icon>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Price Column -->
|
<!-- Price Column -->
|
||||||
<ng-container matColumnDef="price">
|
<ng-container matColumnDef="price">
|
||||||
<th mat-header-cell *matHeaderCellDef>Prix</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Prix</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.price | currency:'EUR' }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.price | currency:'EUR' }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Quantity Column -->
|
<!-- Quantity Column -->
|
||||||
<ng-container matColumnDef="quantity">
|
<ng-container matColumnDef="quantity">
|
||||||
<th mat-header-cell *matHeaderCellDef>Quantité</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Quantité</th>
|
||||||
<td mat-cell *matCellDef="let product">{{ product.quantity }}</td>
|
<td mat-cell *matCellDef="let product">{{ product.quantity }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef class="actions-head">Actions</th>
|
||||||
<td mat-cell *matCellDef="let product" class="actions-cell">
|
<td mat-cell *matCellDef="let product" class="actions-cell">
|
||||||
<button mat-icon-button (click)="onEdit(product)">
|
<button mat-icon-button (click)="onEdit(product)" aria-label="Modifier">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-icon-button color="warn" (click)="onDelete(product)">
|
<button mat-icon-button color="warn" (click)="onDelete(product)" aria-label="Supprimer">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5,10,25]" showFirstLastButtons></mat-paginator>
|
<!-- 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) {
|
@if (!products || products.length === 0) {
|
||||||
<div class="no-products">
|
<div class="no-products gl-block">Aucun produit trouvé.</div>
|
||||||
Aucun produit trouvé.
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import {MatSort} from '@angular/material/sort';
|
|||||||
import {Product} from '../../interfaces/product';
|
import {Product} from '../../interfaces/product';
|
||||||
import {ProductService} from '../../services/app/product.service';
|
import {ProductService} from '../../services/app/product.service';
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
import {ProductDialogComponent} from '../product-dialog/product-dialog.component';
|
|
||||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||||
import {MatFormField} from '@angular/material/form-field';
|
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
import {MatInput} from '@angular/material/input';
|
import {MatInput} from '@angular/material/input';
|
||||||
import {CurrencyPipe} from '@angular/common';
|
import {CurrencyPipe} from '@angular/common';
|
||||||
@@ -49,7 +48,8 @@ import {CurrencyPipe} from '@angular/common';
|
|||||||
MatSort,
|
MatSort,
|
||||||
MatTable,
|
MatTable,
|
||||||
MatHeaderCellDef,
|
MatHeaderCellDef,
|
||||||
CurrencyPipe
|
CurrencyPipe,
|
||||||
|
MatLabel
|
||||||
],
|
],
|
||||||
styleUrls: ['./products-list.component.css']
|
styleUrls: ['./products-list.component.css']
|
||||||
})
|
})
|
||||||
@@ -100,31 +100,11 @@ export class ProductsListComponent implements OnInit, AfterViewInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAdd(): void {
|
onAdd(): void {
|
||||||
const ref = this.dialog.open(ProductDialogComponent, {
|
|
||||||
data: {product: {id: '', name: '', brand: undefined}},
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Product) => {
|
|
||||||
if (result) {
|
|
||||||
this.add.emit(result);
|
|
||||||
this.productService.addProduct(result).subscribe(() => this.loadProducts());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEdit(product: Product): void {
|
onEdit(product: Product): void {
|
||||||
const ref = this.dialog.open(ProductDialogComponent, {
|
|
||||||
data: {product: {...product}},
|
|
||||||
width: '420px'
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe((result?: Product) => {
|
|
||||||
if (result) {
|
|
||||||
this.edit.emit(result);
|
|
||||||
this.productService.updateProduct((product as any).id, result).subscribe(() => this.loadProducts());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDelete(product: Product): void {
|
onDelete(product: Product): void {
|
||||||
|
|||||||
@@ -178,12 +178,12 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
this.brandSubscription = this.brandService.getBrands().subscribe({
|
this.brandSubscription = this.brandService.getAll().subscribe({
|
||||||
next: (brands: Brand[]) => {
|
next: (brands: Brand[]) => {
|
||||||
this.brands = this.normalizeIds(brands, 'id');
|
this.brands = this.normalizeIds(brands, 'id');
|
||||||
this.filteredBrands = [...this.brands];
|
this.filteredBrands = [...this.brands];
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error: any) => {
|
||||||
console.error('Error fetching brands:', error);
|
console.error('Error fetching brands:', error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
@@ -191,12 +191,12 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.platformSubscription = this.platformService.getPlatforms().subscribe({
|
this.platformSubscription = this.platformService.getAll().subscribe({
|
||||||
next: (platforms: Platform[]) => {
|
next: (platforms: Platform[]) => {
|
||||||
this.platforms = this.normalizeIds(platforms, 'id');
|
this.platforms = this.normalizeIds(platforms, 'id');
|
||||||
this.filteredPlatforms = [...this.platforms];
|
this.filteredPlatforms = [...this.platforms];
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error: any) => {
|
||||||
console.error('Error fetching platforms:', error);
|
console.error('Error fetching platforms:', error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
@@ -204,11 +204,11 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.categorySubscription = this.categoryService.getCategories().subscribe({
|
this.categorySubscription = this.categoryService.getAll().subscribe({
|
||||||
next: (categories: Category[]) => {
|
next: (categories: Category[]) => {
|
||||||
this.categories = this.normalizeIds(categories, 'id');
|
this.categories = this.normalizeIds(categories, 'id');
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error: any) => {
|
||||||
console.error('Error fetching categories:', error);
|
console.error('Error fetching categories:', error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
import {Brand} from '../../interfaces/brand';
|
import {Brand} from '../../interfaces/brand';
|
||||||
|
import {CrudService} from '../crud.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class BrandService {
|
export class BrandService implements CrudService<Brand> {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/app/brands';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/brands';
|
||||||
|
|
||||||
getBrands() {
|
getAll(): Observable<Brand[]> {
|
||||||
return this.http.get<Brand[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Brand[]>(this.BASE_URL, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
addBrand(brand: Brand) {
|
add(item: Brand): Observable<Brand> {
|
||||||
console.log("Adding brand:", brand);
|
console.log('Adding brand:', item);
|
||||||
return this.http.post(this.BASE_URL, brand, {withCredentials: true});
|
return this.http.post<Brand>(this.BASE_URL, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBrand(id: string, brand: Brand) {
|
update(id: string | number, item: Brand): Observable<Brand> {
|
||||||
console.log("Updating brand:", id, brand);
|
console.log('Updating brand:', id, item);
|
||||||
return this.http.put(`${this.BASE_URL}/${id}`, brand, {withCredentials: true});
|
return this.http.put<Brand>(`${this.BASE_URL}/${id}`, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBrand(id: string) {
|
delete(id: string | number): Observable<void> {
|
||||||
console.log("Deleting brand:", id);
|
console.log('Deleting brand:', id);
|
||||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
return this.http.delete<void>(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
import {Category} from '../../interfaces/category';
|
import {Category} from '../../interfaces/category';
|
||||||
|
import {CrudService} from '../crud.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class CategoryService {
|
export class CategoryService implements CrudService<Category> {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/app/categories';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/categories';
|
||||||
|
|
||||||
getCategories() {
|
getAll(): Observable<Category[]> {
|
||||||
return this.http.get<Category[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Category[]>(this.BASE_URL, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
addCategory(category: Category) {
|
add(item: Category): Observable<Category> {
|
||||||
console.log("Adding category:", category);
|
console.log('Adding category:', item);
|
||||||
return this.http.post(this.BASE_URL, category, {withCredentials: true});
|
return this.http.post<Category>(this.BASE_URL, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCategory(id: string, category: Category) {
|
update(id: string | number, item: Category): Observable<Category> {
|
||||||
console.log("Updating category:", id, category);
|
console.log('Updating category:', id, item);
|
||||||
return this.http.put(`${this.BASE_URL}/${id}`, category, {withCredentials: true});
|
return this.http.put<Category>(`${this.BASE_URL}/${id}`, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCategory(id: string) {
|
delete(id: string | number): Observable<void> {
|
||||||
console.log("Deleting category:", id);
|
console.log('Deleting category:', id);
|
||||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
return this.http.delete<void>(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
import {Platform} from '../../interfaces/platform';
|
import {Platform} from '../../interfaces/platform';
|
||||||
|
import {CrudService} from '../crud.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class PlatformService {
|
export class PlatformService implements CrudService<Platform> {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/app/platforms';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/platforms';
|
||||||
|
|
||||||
getPlatforms() {
|
getAll(): Observable<Platform[]> {
|
||||||
return this.http.get<Platform[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Platform[]>(this.BASE_URL, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlatform(platform: Platform) {
|
add(item: Platform): Observable<Platform> {
|
||||||
console.log("Adding platform:", platform);
|
console.log('Adding platform:', item);
|
||||||
return this.http.post(this.BASE_URL, platform, {withCredentials: true});
|
return this.http.post<Platform>(this.BASE_URL, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlatform(id: string, platform: Platform) {
|
update(id: string | number, item: Platform): Observable<Platform> {
|
||||||
console.log("Updating platform:", id, platform);
|
console.log('Updating platform:', id, item);
|
||||||
return this.http.put(`${this.BASE_URL}/${id}`, platform, {withCredentials: true});
|
return this.http.put<Platform>(`${this.BASE_URL}/${id}`, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePlatform(id: string) {
|
delete(id: string | number): Observable<void> {
|
||||||
console.log("Deleting platform:", id);
|
console.log('Deleting platform:', id);
|
||||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
return this.http.delete<void>(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
client/src/app/services/crud.service.ts
Normal file
8
client/src/app/services/crud.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface CrudService<T> {
|
||||||
|
getAll(): Observable<T[]>;
|
||||||
|
add(item: T): Observable<T>;
|
||||||
|
update(id: string | number, item: T): Observable<T>;
|
||||||
|
delete(id: string | number): Observable<void>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user