add Categories management: create CategoriesList component, update admin navbar, and integrate category handling in product forms
This commit is contained in:
@@ -8,6 +8,7 @@ import {authOnlyCanActivate, authOnlyCanMatch} from './guards/auth-only.guard';
|
|||||||
import {AdminComponent} from './pages/admin/admin.component';
|
import {AdminComponent} from './pages/admin/admin.component';
|
||||||
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard';
|
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard';
|
||||||
import {AddProductComponent} from './pages/add-product/add-product.component';
|
import {AddProductComponent} from './pages/add-product/add-product.component';
|
||||||
|
import {ProductsComponent} from './pages/products/products.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -48,6 +49,12 @@ export const routes: Routes = [
|
|||||||
canMatch: [adminOnlyCanMatch],
|
canMatch: [adminOnlyCanMatch],
|
||||||
canActivate: [adminOnlyCanActivate]
|
canActivate: [adminOnlyCanActivate]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path : 'products',
|
||||||
|
component: ProductsComponent,
|
||||||
|
canMatch: [authOnlyCanMatch],
|
||||||
|
canActivate: [authOnlyCanActivate]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path : 'add-product',
|
path : 'add-product',
|
||||||
component: AddProductComponent,
|
component: AddProductComponent,
|
||||||
|
|||||||
@@ -5,5 +5,7 @@
|
|||||||
<mat-tab label="Plateformes">
|
<mat-tab label="Plateformes">
|
||||||
<app-platforms-list></app-platforms-list>
|
<app-platforms-list></app-platforms-list>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab label="Catégories">Catégories</mat-tab>
|
<mat-tab label="Catégories">
|
||||||
|
<app-categories-list></app-categories-list>
|
||||||
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 {BrandsListComponent} from '../brands-list/brands-list.component';
|
||||||
import {PlatformsListComponent} from '../platforms-list/platforms-list.component';
|
import {PlatformsListComponent} from '../platforms-list/platforms-list.component';
|
||||||
|
import {CategoriesListComponent} from '../categories-list/categories-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-navbar',
|
selector: 'app-admin-navbar',
|
||||||
@@ -18,7 +19,8 @@ import {PlatformsListComponent} from '../platforms-list/platforms-list.component
|
|||||||
MatTabGroup,
|
MatTabGroup,
|
||||||
MatTab,
|
MatTab,
|
||||||
BrandsListComponent,
|
BrandsListComponent,
|
||||||
PlatformsListComponent
|
PlatformsListComponent,
|
||||||
|
CategoriesListComponent
|
||||||
],
|
],
|
||||||
templateUrl: './admin-navbar.component.html',
|
templateUrl: './admin-navbar.component.html',
|
||||||
styleUrl: './admin-navbar.component.css'
|
styleUrl: './admin-navbar.component.css'
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
@if (!brands || brands.length === 0) {
|
@if (!brands || brands.length === 0) {
|
||||||
<div class="no-brands">
|
<div class="no-brands">
|
||||||
Aucune marque trouvée.
|
Aucune plateforme trouvée.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {MatButton, MatIconButton} from '@angular/material/button';
|
|||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
import {MatFormField} from '@angular/material/form-field';
|
import {MatFormField} from '@angular/material/form-field';
|
||||||
import {MatInput} from '@angular/material/input';
|
import {MatInput} from '@angular/material/input';
|
||||||
import {BrandService} from '../../services/brand/brand.service';
|
import {BrandService} from '../../services/app/brand.service';
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
import { BrandDialogComponent } from '../brand-dialog/brand-dialog.component';
|
import { BrandDialogComponent } from '../brand-dialog/brand-dialog.component';
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@ export class BrandsListComponent implements OnInit, AfterViewInit, OnChanges {
|
|||||||
next: (brands:Brand[]) => {
|
next: (brands:Brand[]) => {
|
||||||
this.brands = brands || []
|
this.brands = brands || []
|
||||||
this.dataSource.data = this.brands;
|
this.dataSource.data = this.brands;
|
||||||
|
console.log("Fetched brands:", this.brands);
|
||||||
},
|
},
|
||||||
error: () => this.brands = []
|
error: () => this.brands = []
|
||||||
});
|
});
|
||||||
@@ -101,7 +102,7 @@ export class BrandsListComponent implements OnInit, AfterViewInit, OnChanges {
|
|||||||
|
|
||||||
onAdd(): void {
|
onAdd(): void {
|
||||||
const ref = this.dialog.open(BrandDialogComponent, {
|
const ref = this.dialog.open(BrandDialogComponent, {
|
||||||
data: { brand: { id: '', name: '' } },
|
data: { brand: { id: '', name: '', brand: undefined } },
|
||||||
width: '420px'
|
width: '420px'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import {Brand} from '../../interfaces/brand';
|
|||||||
import {Platform} from '../../interfaces/platform';
|
import {Platform} from '../../interfaces/platform';
|
||||||
import {MatOption} from '@angular/material/core';
|
import {MatOption} from '@angular/material/core';
|
||||||
import {MatSelect} from '@angular/material/select';
|
import {MatSelect} from '@angular/material/select';
|
||||||
import {BrandService} from '../../services/brand/brand.service';
|
import {BrandService} from '../../services/app/brand.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-platform-dialog',
|
selector: 'app-platform-dialog',
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ button.mat-icon-button {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-brands {
|
.no-platforms {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
color: rgba(0,0,0,0.6);
|
color: rgba(0,0,0,0.6);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {MatButton, MatIconButton} from '@angular/material/button';
|
|||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
import {MatFormField} from '@angular/material/form-field';
|
import {MatFormField} from '@angular/material/form-field';
|
||||||
import {MatInput} from '@angular/material/input';
|
import {MatInput} from '@angular/material/input';
|
||||||
import {PlatformService} from '../../services/platform/platform.service';
|
import {PlatformService} from '../../services/app/platform.service';
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
import { PlatformDialogComponent } from '../platform-dialog/platform-dialog.component';
|
import { PlatformDialogComponent } from '../platform-dialog/platform-dialog.component';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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 @@
|
|||||||
|
<p>product-form works!</p>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Title Column -->
|
||||||
|
<ng-container matColumnDef="title">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Nom</th>
|
||||||
|
<td mat-cell *matCellDef="let product">{{ product.title }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Description Column -->
|
||||||
|
<ng-container matColumnDef="description">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||||
|
<td mat-cell *matCellDef="let product">{{ product.description }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Category Column -->
|
||||||
|
<ng-container matColumnDef="category">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>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>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>É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>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>Quantité</th>
|
||||||
|
<td mat-cell *matCellDef="let product">{{ product.quantity }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Actions Column -->
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell *matCellDef="let product" class="actions-cell">
|
||||||
|
<button mat-icon-button (click)="onEdit(product)">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button color="warn" (click)="onDelete(product)">
|
||||||
|
<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 (!products || products.length === 0) {
|
||||||
|
<div class="no-products">
|
||||||
|
Aucun produit trouvé.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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 {ProductDialogComponent} from '../product-dialog/product-dialog.component';
|
||||||
|
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||||
|
import {MatFormField} 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
|
||||||
|
],
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
this.delete.emit(product);
|
||||||
|
this.productService.deleteProduct((product as any).id).subscribe(() => this.loadProducts());
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(value: string): void {
|
||||||
|
this.dataSource.filter = (value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
4
client/src/app/interfaces/category.ts
Normal file
4
client/src/app/interfaces/category.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Category {
|
||||||
|
id: string | number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
5
client/src/app/interfaces/condition.ts
Normal file
5
client/src/app/interfaces/condition.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Condition {
|
||||||
|
id: string | number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
16
client/src/app/interfaces/product.ts
Normal file
16
client/src/app/interfaces/product.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {Category} from './category';
|
||||||
|
import {Platform} from './platform';
|
||||||
|
import {Condition} from './condition';
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: string | number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
complete: boolean;
|
||||||
|
manualIncluded: boolean;
|
||||||
|
category: Category | undefined;
|
||||||
|
platform: Platform | undefined;
|
||||||
|
condition: Condition | undefined;
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
name="title"
|
name="title"
|
||||||
formControlName="title"
|
formControlName="title"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ceci est un titre"
|
|
||||||
required>
|
required>
|
||||||
@if (isFieldInvalid('title')) {
|
@if (isFieldInvalid('title')) {
|
||||||
<mat-error>{{ getFieldError('title') }}</mat-error>
|
<mat-error>{{ getFieldError('title') }}</mat-error>
|
||||||
@@ -39,29 +38,29 @@
|
|||||||
<!-- Category -->
|
<!-- Category -->
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Catégorie</mat-label>
|
<mat-label>Catégorie</mat-label>
|
||||||
<mat-select disableRipple>
|
<mat-select formControlName="category" disableRipple>
|
||||||
<mat-option value="1">Option 1</mat-option>
|
@for (category of categories; track category.id) {
|
||||||
<mat-option value="2">Option 2</mat-option>
|
<mat-option [value]="category">{{ category.name }}</mat-option>
|
||||||
<mat-option value="3">Option 3</mat-option>
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Condition -->
|
<!-- Condition -->
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>État</mat-label>
|
<mat-label>État</mat-label>
|
||||||
<mat-select disableRipple>
|
<mat-select formControlName="condition" disableRipple>
|
||||||
<mat-option value="1">Option 1</mat-option>
|
@for (condition of conditions; track condition.id) {
|
||||||
<mat-option value="2">Option 2</mat-option>
|
<mat-option [value]="condition">{{ condition.displayName }}</mat-option>
|
||||||
<mat-option value="3">Option 3</mat-option>
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Brand -->
|
<!-- Brand -->
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Marque</mat-label>
|
<mat-label>Marque</mat-label>
|
||||||
<mat-select disableRipple>
|
<mat-select formControlName="brand" [compareWith]="compareById" disableRipple>
|
||||||
@for (brand of brands; track brand.id) {
|
@for (brand of filteredBrands; track brand.id) {
|
||||||
<mat-option [value]="brand">{{ brand.name }}</mat-option>
|
<mat-option [value]="brand.id">{{ brand.name }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -69,9 +68,9 @@
|
|||||||
<!-- Platform -->
|
<!-- Platform -->
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Plateforme</mat-label>
|
<mat-label>Plateforme</mat-label>
|
||||||
<mat-select disableRipple>
|
<mat-select formControlName="platform" [compareWith]="compareById" disableRipple>
|
||||||
@for (platform of platforms; track platform.id) {
|
@for (platform of filteredPlatforms; track platform.id) {
|
||||||
<mat-option [value]="platform">{{ platform.name }}</mat-option>
|
<mat-option [value]="platform.id">{{ platform.name }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
AbstractControl,
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule, ValidatorFn,
|
||||||
Validators
|
Validators
|
||||||
} from "@angular/forms";
|
} from "@angular/forms";
|
||||||
import {MatButton} from "@angular/material/button";
|
import {MatButton} from "@angular/material/button";
|
||||||
@@ -20,12 +21,17 @@ import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
|
|||||||
import {MatInput} from "@angular/material/input";
|
import {MatInput} from "@angular/material/input";
|
||||||
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
||||||
import {MatOption, MatSelect} from '@angular/material/select';
|
import {MatOption, MatSelect} from '@angular/material/select';
|
||||||
import {Router, RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {BrandService} from '../../services/brand/brand.service';
|
import {BrandService} from '../../services/app/brand.service';
|
||||||
import {Brand} from '../../interfaces/brand';
|
import {Brand} from '../../interfaces/brand';
|
||||||
import {PlatformService} from '../../services/platform/platform.service';
|
import {PlatformService} from '../../services/app/platform.service';
|
||||||
import {Platform} from '../../interfaces/platform';
|
import {Platform} from '../../interfaces/platform';
|
||||||
|
import {Category} from '../../interfaces/category';
|
||||||
|
import {CategoryService} from '../../services/app/category.service';
|
||||||
|
import {ConditionService} from '../../services/app/condition.service';
|
||||||
|
import {Condition} from '../../interfaces/condition';
|
||||||
|
import {ProductService} from '../../services/app/product.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-product',
|
selector: 'app-add-product',
|
||||||
@@ -61,12 +67,28 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
brands: Brand[] = [];
|
brands: Brand[] = [];
|
||||||
platforms: Platform[] = [];
|
platforms: Platform[] = [];
|
||||||
|
categories: Category[] = [];
|
||||||
|
conditions: Condition[] = [];
|
||||||
|
|
||||||
private readonly router: Router = inject(Router);
|
filteredBrands: Brand[] = [];
|
||||||
|
filteredPlatforms: Platform[] = [];
|
||||||
|
|
||||||
|
private addProductSubscription: Subscription | null = null;
|
||||||
|
|
||||||
|
private brandControlSubscription: Subscription | null = null;
|
||||||
|
private platformControlSubscription: Subscription | null = null;
|
||||||
|
|
||||||
|
private brandSubscription: Subscription | null = null;
|
||||||
|
private platformSubscription: Subscription | null = null;
|
||||||
|
private categorySubscription: Subscription | null = null;
|
||||||
|
private conditionSubscription: Subscription | null = null;
|
||||||
|
|
||||||
private readonly addProductSubscription: Subscription | null = null;
|
|
||||||
private readonly brandService: BrandService = inject(BrandService);
|
private readonly brandService: BrandService = inject(BrandService);
|
||||||
private readonly platformService = inject(PlatformService);
|
private readonly platformService = inject(PlatformService);
|
||||||
|
private readonly categoryService = inject(CategoryService);
|
||||||
|
private readonly conditionService = inject(ConditionService);
|
||||||
|
|
||||||
|
private readonly productService = inject(ProductService);
|
||||||
|
|
||||||
constructor(private readonly formBuilder: FormBuilder) {
|
constructor(private readonly formBuilder: FormBuilder) {
|
||||||
this.addProductForm = this.formBuilder.group({
|
this.addProductForm = this.formBuilder.group({
|
||||||
@@ -74,13 +96,13 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.minLength(3),
|
Validators.minLength(3),
|
||||||
Validators.maxLength(50),
|
Validators.maxLength(50),
|
||||||
Validators.pattern('^[a-zA-Z]+$')
|
Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
|
||||||
]],
|
]],
|
||||||
description: ['', [
|
description: ['', [
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.minLength(10),
|
Validators.minLength(10),
|
||||||
Validators.maxLength(255),
|
Validators.maxLength(255),
|
||||||
Validators.pattern('^[a-zA-Z]+$')
|
Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
|
||||||
]],
|
]],
|
||||||
category: ['', [
|
category: ['', [
|
||||||
Validators.required
|
Validators.required
|
||||||
@@ -88,38 +110,78 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
condition: ['', [
|
condition: ['', [
|
||||||
Validators.required
|
Validators.required
|
||||||
]],
|
]],
|
||||||
|
// stocker des ids (string|number) dans les controls
|
||||||
brand: ['', [
|
brand: ['', [
|
||||||
Validators.required
|
Validators.required
|
||||||
]],
|
]],
|
||||||
platform: ['', [
|
platform: ['', [
|
||||||
Validators.required
|
Validators.required
|
||||||
]],
|
]],
|
||||||
complete: [true,
|
complete: [true],
|
||||||
Validators.requiredTrue
|
manual: [true],
|
||||||
],
|
|
||||||
manual: [true,
|
|
||||||
Validators.requiredTrue
|
|
||||||
],
|
|
||||||
price: ['', [
|
price: ['', [
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.min(0),
|
Validators.pattern(/^\d+([.,]\d{1,2})?$/),
|
||||||
Validators.max(9999),
|
this.priceRangeValidator(0, 9999)
|
||||||
Validators.pattern('^[0-9]+$')
|
|
||||||
]],
|
]],
|
||||||
quantity: ['', [
|
quantity: ['', [
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.min(1),
|
Validators.min(1),
|
||||||
Validators.max(999),
|
Validators.max(999),
|
||||||
Validators.pattern('^[0-9]+$')
|
Validators.pattern(/^\d+$/)
|
||||||
]]
|
]]
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeIds<T extends Record<string, any>>(items: T[] | undefined, idKey = 'id'): T[] {
|
||||||
|
return (items || []).map((it, i) => ({
|
||||||
|
...it,
|
||||||
|
[idKey]: (it[idKey] ?? i)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlatformBrandId(platform: any): string | number | undefined {
|
||||||
|
if (!platform) return undefined;
|
||||||
|
const maybe = platform.brand ?? platform['brand_id'] ?? platform['brandId'];
|
||||||
|
if (maybe == null) return undefined;
|
||||||
|
|
||||||
|
if (typeof maybe === 'object') {
|
||||||
|
if (maybe.id != null) return maybe.id;
|
||||||
|
if (maybe.name != null) {
|
||||||
|
const found = this.brands.find(b =>
|
||||||
|
String(b.name).toLowerCase() === String(maybe.name).toLowerCase()
|
||||||
|
|| String(b.id) === String(maybe.name)
|
||||||
|
);
|
||||||
|
return found?.id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asStr = String(maybe);
|
||||||
|
const match = this.brands.find(b =>
|
||||||
|
String(b.id) === asStr || String(b.name).toLowerCase() === asStr.toLowerCase()
|
||||||
|
);
|
||||||
|
return match?.id ?? maybe;
|
||||||
|
}
|
||||||
|
|
||||||
|
private priceRangeValidator(min: number, max: number): ValidatorFn {
|
||||||
|
return (control: AbstractControl) => {
|
||||||
|
const val = control.value;
|
||||||
|
if (val === null || val === undefined || val === '') return null;
|
||||||
|
const normalized = String(val).replace(',', '.').trim();
|
||||||
|
const num = Number.parseFloat(normalized);
|
||||||
|
if (Number.isNaN(num)) return {pattern: true};
|
||||||
|
return (num < min || num > max) ? {range: {min, max, actual: num}} : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.brandService.getBrands().subscribe({
|
|
||||||
next: (brands) => {
|
this.brandSubscription = this.brandService.getBrands().subscribe({
|
||||||
this.brands = brands;
|
next: (brands: Brand[]) => {
|
||||||
|
this.brands = this.normalizeIds(brands, 'id');
|
||||||
|
this.filteredBrands = [...this.brands];
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error fetching brands:', error);
|
console.error('Error fetching brands:', error);
|
||||||
@@ -129,9 +191,10 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.platformService.getPlatforms().subscribe({
|
this.platformSubscription = this.platformService.getPlatforms().subscribe({
|
||||||
next: (platforms) => {
|
next: (platforms: Platform[]) => {
|
||||||
this.platforms = platforms;
|
this.platforms = this.normalizeIds(platforms, 'id');
|
||||||
|
this.filteredPlatforms = [...this.platforms];
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error fetching platforms:', error);
|
console.error('Error fetching platforms:', error);
|
||||||
@@ -140,21 +203,118 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
console.log('Finished fetching platforms:', this.platforms);
|
console.log('Finished fetching platforms:', this.platforms);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.categorySubscription = this.categoryService.getCategories().subscribe({
|
||||||
|
next: (categories: Category[]) => {
|
||||||
|
this.categories = this.normalizeIds(categories, 'id');
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error fetching categories:', error);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
console.log('Finished fetching categories:', this.categories);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.conditionSubscription = this.conditionService.getConditions().subscribe({
|
||||||
|
next: (conditions: Condition[]) => {
|
||||||
|
this.conditions = this.normalizeIds(conditions, 'id');
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error fetching conditions:', error);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
console.log('Finished fetching conditions:', this.conditions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const brandControl = this.addProductForm.get('brand');
|
||||||
|
const platformControl = this.addProductForm.get('platform');
|
||||||
|
|
||||||
|
this.brandControlSubscription = brandControl?.valueChanges.subscribe((brandId) => {
|
||||||
|
if (brandId != null && brandId !== '') {
|
||||||
|
const brandIdStr = String(brandId);
|
||||||
|
this.filteredPlatforms = this.platforms.filter(p => {
|
||||||
|
const pBid = this.getPlatformBrandId(p);
|
||||||
|
return pBid != null && String(pBid) === brandIdStr;
|
||||||
|
});
|
||||||
|
const curPlatformId = platformControl?.value;
|
||||||
|
if (curPlatformId != null && !this.filteredPlatforms.some(p => String(p.id) === String(curPlatformId))) {
|
||||||
|
platformControl?.setValue(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filteredPlatforms = [...this.platforms];
|
||||||
|
}
|
||||||
|
}) ?? null;
|
||||||
|
|
||||||
|
this.platformControlSubscription = platformControl?.valueChanges.subscribe((platformId) => {
|
||||||
|
if (platformId != null && platformId !== '') {
|
||||||
|
const platformObj = this.platforms.find(p => String(p.id) === String(platformId));
|
||||||
|
const pBrandId = this.getPlatformBrandId(platformObj);
|
||||||
|
if (pBrandId != null) {
|
||||||
|
const pBrandIdStr = String(pBrandId);
|
||||||
|
this.filteredBrands = this.brands.filter(b => String(b.id) === pBrandIdStr);
|
||||||
|
const curBrandId = brandControl?.value;
|
||||||
|
if (curBrandId != null && String(curBrandId) !== pBrandIdStr) {
|
||||||
|
brandControl?.setValue(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filteredBrands = [...this.brands];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filteredBrands = [...this.brands];
|
||||||
|
}
|
||||||
|
}) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.addProductSubscription?.unsubscribe();
|
this.addProductSubscription?.unsubscribe();
|
||||||
|
this.brandControlSubscription?.unsubscribe();
|
||||||
|
this.platformControlSubscription?.unsubscribe();
|
||||||
|
this.brandSubscription?.unsubscribe();
|
||||||
|
this.platformSubscription?.unsubscribe();
|
||||||
|
this.categorySubscription?.unsubscribe();
|
||||||
|
this.conditionSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
onProductAdd() {
|
onProductAdd() {
|
||||||
|
|
||||||
this.isSubmitted = true;
|
this.isSubmitted = true;
|
||||||
|
|
||||||
if (this.addProductForm.valid) {
|
if (this.addProductForm.valid) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const productData = this.addProductForm.value;
|
const raw = this.addProductForm.value;
|
||||||
|
|
||||||
|
const priceStr = raw.price ?? '';
|
||||||
|
const priceNum = Number(String(priceStr).replace(',', '.').trim());
|
||||||
|
if (Number.isNaN(priceNum)) {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.addProductForm.get('price')?.setErrors({pattern: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantityNum = Number(raw.quantity);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...raw,
|
||||||
|
price: priceNum,
|
||||||
|
quantity: quantityNum
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addProductSubscription = this.productService.addProduct(payload).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
console.log("Product added successfully:", response);
|
||||||
|
this.addProductForm.reset();
|
||||||
|
this.isSubmitted = false;
|
||||||
alert("Produit ajouté avec succès !");
|
alert("Produit ajouté avec succès !");
|
||||||
console.log(productData);
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error("Error adding product:", error);
|
||||||
|
alert("Une erreur est survenue lors de l'ajout du produit.");
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,4 +334,12 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compareById = (a: any, b: any) => {
|
||||||
|
if (a == null || b == null) return a === b;
|
||||||
|
if (typeof a !== 'object' || typeof b !== 'object') {
|
||||||
|
return String(a) === String(b);
|
||||||
|
}
|
||||||
|
return String(a.id ?? a) === String(b.id ?? b);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1
client/src/app/pages/products/products.component.html
Normal file
1
client/src/app/pages/products/products.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<app-products-list></app-products-list>
|
||||||
17
client/src/app/pages/products/products.component.ts
Normal file
17
client/src/app/pages/products/products.component.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {ProductsListComponent} from '../../components/products-list/products-list.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-products',
|
||||||
|
templateUrl: './products.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ProductsListComponent
|
||||||
|
],
|
||||||
|
styleUrls: ['./products.component.css']
|
||||||
|
})
|
||||||
|
export class ProductsComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import {Brand} from '../../interfaces/brand';
|
|||||||
export class BrandService {
|
export class BrandService {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/brands';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/brands';
|
||||||
|
|
||||||
getBrands() {
|
getBrands() {
|
||||||
return this.http.get<Brand[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Brand[]>(this.BASE_URL, {withCredentials: true});
|
||||||
31
client/src/app/services/app/category.service.ts
Normal file
31
client/src/app/services/app/category.service.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Category} from '../../interfaces/category';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CategoryService {
|
||||||
|
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly BASE_URL = 'http://localhost:3000/api/app/categories';
|
||||||
|
|
||||||
|
getCategories() {
|
||||||
|
return this.http.get<Category[]>(this.BASE_URL, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCategory(category: Category) {
|
||||||
|
console.log("Adding category:", category);
|
||||||
|
return this.http.post(this.BASE_URL, category, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCategory(id: string, category: Category) {
|
||||||
|
console.log("Updating category:", id, category);
|
||||||
|
return this.http.put(`${this.BASE_URL}/${id}`, category, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCategory(id: string) {
|
||||||
|
console.log("Deleting category:", id);
|
||||||
|
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
31
client/src/app/services/app/condition.service.ts
Normal file
31
client/src/app/services/app/condition.service.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Condition} from '../../interfaces/condition';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ConditionService {
|
||||||
|
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly BASE_URL = 'http://localhost:3000/api/app/conditions';
|
||||||
|
|
||||||
|
getConditions() {
|
||||||
|
return this.http.get<Condition[]>(this.BASE_URL, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCondition(condition: Condition) {
|
||||||
|
console.log("Adding condition:", condition);
|
||||||
|
return this.http.post(this.BASE_URL, condition, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCondition(id: string, condition: Condition) {
|
||||||
|
console.log("Updating condition:", id, condition);
|
||||||
|
return this.http.put(`${this.BASE_URL}/${id}`, condition, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCondition(id: string) {
|
||||||
|
console.log("Deleting condition:", id);
|
||||||
|
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import {Platform} from '../../interfaces/platform';
|
|||||||
export class PlatformService {
|
export class PlatformService {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/platforms';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/platforms';
|
||||||
|
|
||||||
getPlatforms() {
|
getPlatforms() {
|
||||||
return this.http.get<Platform[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Platform[]>(this.BASE_URL, {withCredentials: true});
|
||||||
31
client/src/app/services/app/product.service.ts
Normal file
31
client/src/app/services/app/product.service.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Product} from '../../interfaces/product';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ProductService {
|
||||||
|
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly BASE_URL = 'http://localhost:3000/api/app/products';
|
||||||
|
|
||||||
|
getProducts() {
|
||||||
|
return this.http.get<Product[]>(this.BASE_URL, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
addProduct(product: Product) {
|
||||||
|
console.log("Adding product:", product);
|
||||||
|
return this.http.post(this.BASE_URL, product, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProduct(id: string, product: Product) {
|
||||||
|
console.log("Updating product:", id, product);
|
||||||
|
return this.http.put(`${this.BASE_URL}/${id}`, product, {withCredentials: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteProduct(id: string) {
|
||||||
|
console.log("Deleting product:", id);
|
||||||
|
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
|
||||||
import {HttpClient} from '@angular/common/http';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class ProductService {
|
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/products';
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user