diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts
index 61efc96..c40560e 100644
--- a/client/src/app/app.routes.ts
+++ b/client/src/app/app.routes.ts
@@ -8,6 +8,7 @@ import {authOnlyCanActivate, authOnlyCanMatch} from './guards/auth-only.guard';
import {AdminComponent} from './pages/admin/admin.component';
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard';
import {AddProductComponent} from './pages/add-product/add-product.component';
+import {ProductsComponent} from './pages/products/products.component';
export const routes: Routes = [
{
@@ -48,6 +49,12 @@ export const routes: Routes = [
canMatch: [adminOnlyCanMatch],
canActivate: [adminOnlyCanActivate]
},
+ {
+ path : 'products',
+ component: ProductsComponent,
+ canMatch: [authOnlyCanMatch],
+ canActivate: [authOnlyCanActivate]
+ },
{
path : 'add-product',
component: AddProductComponent,
diff --git a/client/src/app/components/admin-navbar/admin-navbar.component.html b/client/src/app/components/admin-navbar/admin-navbar.component.html
index ec470ab..c9dba51 100644
--- a/client/src/app/components/admin-navbar/admin-navbar.component.html
+++ b/client/src/app/components/admin-navbar/admin-navbar.component.html
@@ -5,5 +5,7 @@
- Catégories
+
+
+
diff --git a/client/src/app/components/admin-navbar/admin-navbar.component.ts b/client/src/app/components/admin-navbar/admin-navbar.component.ts
index 818dea2..2fcac2c 100644
--- a/client/src/app/components/admin-navbar/admin-navbar.component.ts
+++ b/client/src/app/components/admin-navbar/admin-navbar.component.ts
@@ -5,6 +5,7 @@ import {RouterLink, RouterLinkActive} from '@angular/router';
import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {BrandsListComponent} from '../brands-list/brands-list.component';
import {PlatformsListComponent} from '../platforms-list/platforms-list.component';
+import {CategoriesListComponent} from '../categories-list/categories-list.component';
@Component({
selector: 'app-admin-navbar',
@@ -18,7 +19,8 @@ import {PlatformsListComponent} from '../platforms-list/platforms-list.component
MatTabGroup,
MatTab,
BrandsListComponent,
- PlatformsListComponent
+ PlatformsListComponent,
+ CategoriesListComponent
],
templateUrl: './admin-navbar.component.html',
styleUrl: './admin-navbar.component.css'
diff --git a/client/src/app/components/brands-list/brands-list.component.html b/client/src/app/components/brands-list/brands-list.component.html
index 1ed48ec..c2a37e6 100644
--- a/client/src/app/components/brands-list/brands-list.component.html
+++ b/client/src/app/components/brands-list/brands-list.component.html
@@ -38,7 +38,7 @@
@if (!brands || brands.length === 0) {
- Aucune marque trouvée.
+ Aucune plateforme trouvée.
}
diff --git a/client/src/app/components/brands-list/brands-list.component.ts b/client/src/app/components/brands-list/brands-list.component.ts
index ee01363..b7e426b 100644
--- a/client/src/app/components/brands-list/brands-list.component.ts
+++ b/client/src/app/components/brands-list/brands-list.component.ts
@@ -25,7 +25,7 @@ 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/brand/brand.service';
+import {BrandService} from '../../services/app/brand.service';
import {MatDialog} from '@angular/material/dialog';
import { BrandDialogComponent } from '../brand-dialog/brand-dialog.component';
@@ -94,6 +94,7 @@ export class BrandsListComponent implements OnInit, AfterViewInit, OnChanges {
next: (brands:Brand[]) => {
this.brands = brands || []
this.dataSource.data = this.brands;
+ console.log("Fetched brands:", this.brands);
},
error: () => this.brands = []
});
@@ -101,7 +102,7 @@ export class BrandsListComponent implements OnInit, AfterViewInit, OnChanges {
onAdd(): void {
const ref = this.dialog.open(BrandDialogComponent, {
- data: { brand: { id: '', name: '' } },
+ data: { brand: { id: '', name: '', brand: undefined } },
width: '420px'
});
diff --git a/client/src/app/components/categories-list/categories-list.component.css b/client/src/app/components/categories-list/categories-list.component.css
new file mode 100644
index 0000000..87a4b85
--- /dev/null
+++ b/client/src/app/components/categories-list/categories-list.component.css
@@ -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;
+ }
+}
diff --git a/client/src/app/components/categories-list/categories-list.component.html b/client/src/app/components/categories-list/categories-list.component.html
new file mode 100644
index 0000000..3c5f4ef
--- /dev/null
+++ b/client/src/app/components/categories-list/categories-list.component.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Nom |
+ {{ category.name }} |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+ @if (!categories || categories.length === 0) {
+
+ Aucune catégorie trouvée.
+
+ }
+
diff --git a/client/src/app/components/categories-list/categories-list.component.ts b/client/src/app/components/categories-list/categories-list.component.ts
new file mode 100644
index 0000000..5d9e0d4
--- /dev/null
+++ b/client/src/app/components/categories-list/categories-list.component.ts
@@ -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();
+ @Output() edit = new EventEmitter();
+ @Output() delete = new EventEmitter();
+
+ displayedColumns: string[] = ['name', 'actions'];
+ dataSource = new MatTableDataSource([]);
+
+ @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();
+ }
+}
diff --git a/client/src/app/components/category-dialog/category-dialog.component.css b/client/src/app/components/category-dialog/category-dialog.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/app/components/category-dialog/category-dialog.component.html b/client/src/app/components/category-dialog/category-dialog.component.html
new file mode 100644
index 0000000..2a05c27
--- /dev/null
+++ b/client/src/app/components/category-dialog/category-dialog.component.html
@@ -0,0 +1,13 @@
+{{ categoryExists ? 'Modifier la catégorie' : 'Nouvelle catégorie' }}
+
+
+
+ Nom
+
+
+
+
+
+
+
+
diff --git a/client/src/app/components/category-dialog/category-dialog.component.ts b/client/src/app/components/category-dialog/category-dialog.component.ts
new file mode 100644
index 0000000..9d864f2
--- /dev/null
+++ b/client/src/app/components/category-dialog/category-dialog.component.ts
@@ -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,
+ @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();
+ }
+}
diff --git a/client/src/app/components/platform-dialog/platform-dialog.component.ts b/client/src/app/components/platform-dialog/platform-dialog.component.ts
index 2d93833..cc3883a 100644
--- a/client/src/app/components/platform-dialog/platform-dialog.component.ts
+++ b/client/src/app/components/platform-dialog/platform-dialog.component.ts
@@ -14,7 +14,7 @@ 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/brand/brand.service';
+import {BrandService} from '../../services/app/brand.service';
@Component({
selector: 'app-platform-dialog',
diff --git a/client/src/app/components/platforms-list/platforms-list.component.css b/client/src/app/components/platforms-list/platforms-list.component.css
index 87a4b85..aeccac5 100644
--- a/client/src/app/components/platforms-list/platforms-list.component.css
+++ b/client/src/app/components/platforms-list/platforms-list.component.css
@@ -45,7 +45,7 @@ button.mat-icon-button {
height: 40px;
}
-.no-brands {
+.no-platforms {
text-align: center;
margin-top: 16px;
color: rgba(0,0,0,0.6);
diff --git a/client/src/app/components/platforms-list/platforms-list.component.ts b/client/src/app/components/platforms-list/platforms-list.component.ts
index ea689fe..309315f 100644
--- a/client/src/app/components/platforms-list/platforms-list.component.ts
+++ b/client/src/app/components/platforms-list/platforms-list.component.ts
@@ -25,7 +25,7 @@ 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/platform/platform.service';
+import {PlatformService} from '../../services/app/platform.service';
import {MatDialog} from '@angular/material/dialog';
import { PlatformDialogComponent } from '../platform-dialog/platform-dialog.component';
diff --git a/client/src/app/components/product-dialog/product-dialog.component.css b/client/src/app/components/product-dialog/product-dialog.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/app/components/product-dialog/product-dialog.component.html b/client/src/app/components/product-dialog/product-dialog.component.html
new file mode 100644
index 0000000..0958e21
--- /dev/null
+++ b/client/src/app/components/product-dialog/product-dialog.component.html
@@ -0,0 +1,13 @@
+{{ productExists ? 'Modifier la plateforme' : 'Nouvelle plateforme' }}
+
+
+
+ Nom
+
+
+
+
+
+
+
+
diff --git a/client/src/app/components/product-dialog/product-dialog.component.ts b/client/src/app/components/product-dialog/product-dialog.component.ts
new file mode 100644
index 0000000..1fad355
--- /dev/null
+++ b/client/src/app/components/product-dialog/product-dialog.component.ts
@@ -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,
+ @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();
+ }
+}
diff --git a/client/src/app/components/product-form/product-form.component.css b/client/src/app/components/product-form/product-form.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/app/components/product-form/product-form.component.html b/client/src/app/components/product-form/product-form.component.html
new file mode 100644
index 0000000..99b2c89
--- /dev/null
+++ b/client/src/app/components/product-form/product-form.component.html
@@ -0,0 +1 @@
+product-form works!
diff --git a/client/src/app/components/product-form/product-form.component.ts b/client/src/app/components/product-form/product-form.component.ts
new file mode 100644
index 0000000..8d9fa7a
--- /dev/null
+++ b/client/src/app/components/product-form/product-form.component.ts
@@ -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 {
+
+}
diff --git a/client/src/app/components/products-list/products-list.component.css b/client/src/app/components/products-list/products-list.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/app/components/products-list/products-list.component.html b/client/src/app/components/products-list/products-list.component.html
new file mode 100644
index 0000000..83556fd
--- /dev/null
+++ b/client/src/app/components/products-list/products-list.component.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Nom |
+ {{ product.title }} |
+
+
+
+
+ Description |
+ {{ product.description }} |
+
+
+
+
+ Catégorie |
+ {{ product.category.name }} |
+
+
+
+
+ Plateforme |
+ {{ product.platform.name }} |
+
+
+
+
+ État |
+ {{ product.condition.displayName }} |
+
+
+
+
+ Complet |
+
+ @if (product.complete) {
+ check_circle
+ } @else {
+ cancel
+ }
+ |
+
+
+
+
+ Notice |
+
+ @if (product.manual) {
+ check_circle
+ } @else {
+ cancel
+ }
+ |
+
+
+
+
+ Prix |
+ {{ product.price | currency:'EUR' }} |
+
+
+
+
+ Quantité |
+ {{ product.quantity }} |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+ @if (!products || products.length === 0) {
+
+ Aucun produit trouvé.
+
+ }
+
diff --git a/client/src/app/components/products-list/products-list.component.ts b/client/src/app/components/products-list/products-list.component.ts
new file mode 100644
index 0000000..48a03e9
--- /dev/null
+++ b/client/src/app/components/products-list/products-list.component.ts
@@ -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();
+ @Output() edit = new EventEmitter();
+ @Output() delete = new EventEmitter();
+
+ displayedColumns: string[] = ['title', 'description', 'category', 'platform', 'condition', 'complete', 'manual', 'price', 'quantity', 'actions'];
+ dataSource = new MatTableDataSource([]);
+
+ @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();
+ }
+}
diff --git a/client/src/app/interfaces/category.ts b/client/src/app/interfaces/category.ts
new file mode 100644
index 0000000..e761b1f
--- /dev/null
+++ b/client/src/app/interfaces/category.ts
@@ -0,0 +1,4 @@
+export interface Category {
+ id: string | number;
+ name: string;
+}
diff --git a/client/src/app/interfaces/condition.ts b/client/src/app/interfaces/condition.ts
new file mode 100644
index 0000000..43687b0
--- /dev/null
+++ b/client/src/app/interfaces/condition.ts
@@ -0,0 +1,5 @@
+export interface Condition {
+ id: string | number;
+ name: string;
+ displayName: string;
+}
diff --git a/client/src/app/interfaces/product.ts b/client/src/app/interfaces/product.ts
new file mode 100644
index 0000000..7138219
--- /dev/null
+++ b/client/src/app/interfaces/product.ts
@@ -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;
+}
diff --git a/client/src/app/pages/add-product/add-product.component.html b/client/src/app/pages/add-product/add-product.component.html
index 84487e4..80f7073 100644
--- a/client/src/app/pages/add-product/add-product.component.html
+++ b/client/src/app/pages/add-product/add-product.component.html
@@ -15,7 +15,6 @@
name="title"
formControlName="title"
type="text"
- placeholder="Ceci est un titre"
required>
@if (isFieldInvalid('title')) {
{{ getFieldError('title') }}
@@ -39,29 +38,29 @@
Catégorie
-
- Option 1
- Option 2
- Option 3
+
+ @for (category of categories; track category.id) {
+ {{ category.name }}
+ }
État
-
- Option 1
- Option 2
- Option 3
+
+ @for (condition of conditions; track condition.id) {
+ {{ condition.displayName }}
+ }
Marque
-
- @for (brand of brands; track brand.id) {
- {{ brand.name }}
+
+ @for (brand of filteredBrands; track brand.id) {
+ {{ brand.name }}
}
@@ -69,9 +68,9 @@
Plateforme
-
- @for (platform of platforms; track platform.id) {
- {{ platform.name }}
+
+ @for (platform of filteredPlatforms; track platform.id) {
+ {{ platform.name }}
}
diff --git a/client/src/app/pages/add-product/add-product.component.ts b/client/src/app/pages/add-product/add-product.component.ts
index e9e4f9c..ac6f109 100644
--- a/client/src/app/pages/add-product/add-product.component.ts
+++ b/client/src/app/pages/add-product/add-product.component.ts
@@ -1,9 +1,10 @@
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {
+ AbstractControl,
FormBuilder,
FormGroup,
FormsModule,
- ReactiveFormsModule,
+ ReactiveFormsModule, ValidatorFn,
Validators
} from "@angular/forms";
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 {MatProgressSpinner} from "@angular/material/progress-spinner";
import {MatOption, MatSelect} from '@angular/material/select';
-import {Router, RouterLink} from '@angular/router';
+import {RouterLink} from '@angular/router';
import {Subscription} from 'rxjs';
-import {BrandService} from '../../services/brand/brand.service';
+import {BrandService} from '../../services/app/brand.service';
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 {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({
selector: 'app-add-product',
@@ -61,12 +67,28 @@ export class AddProductComponent implements OnInit, OnDestroy {
brands: Brand[] = [];
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 platformService = inject(PlatformService);
+ private readonly categoryService = inject(CategoryService);
+ private readonly conditionService = inject(ConditionService);
+
+ private readonly productService = inject(ProductService);
constructor(private readonly formBuilder: FormBuilder) {
this.addProductForm = this.formBuilder.group({
@@ -74,13 +96,13 @@ export class AddProductComponent implements OnInit, OnDestroy {
Validators.required,
Validators.minLength(3),
Validators.maxLength(50),
- Validators.pattern('^[a-zA-Z]+$')
+ Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
]],
description: ['', [
Validators.required,
Validators.minLength(10),
Validators.maxLength(255),
- Validators.pattern('^[a-zA-Z]+$')
+ Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
]],
category: ['', [
Validators.required
@@ -88,38 +110,78 @@ export class AddProductComponent implements OnInit, OnDestroy {
condition: ['', [
Validators.required
]],
+ // stocker des ids (string|number) dans les controls
brand: ['', [
Validators.required
]],
platform: ['', [
Validators.required
]],
- complete: [true,
- Validators.requiredTrue
- ],
- manual: [true,
- Validators.requiredTrue
- ],
+ complete: [true],
+ manual: [true],
price: ['', [
Validators.required,
- Validators.min(0),
- Validators.max(9999),
- Validators.pattern('^[0-9]+$')
+ Validators.pattern(/^\d+([.,]\d{1,2})?$/),
+ this.priceRangeValidator(0, 9999)
]],
quantity: ['', [
Validators.required,
Validators.min(1),
Validators.max(999),
- Validators.pattern('^[0-9]+$')
+ Validators.pattern(/^\d+$/)
]]
},
);
}
+ private normalizeIds>(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 {
- this.brandService.getBrands().subscribe({
- next: (brands) => {
- this.brands = brands;
+
+ this.brandSubscription = this.brandService.getBrands().subscribe({
+ next: (brands: Brand[]) => {
+ this.brands = this.normalizeIds(brands, 'id');
+ this.filteredBrands = [...this.brands];
},
error: (error) => {
console.error('Error fetching brands:', error);
@@ -129,9 +191,10 @@ export class AddProductComponent implements OnInit, OnDestroy {
}
});
- this.platformService.getPlatforms().subscribe({
- next: (platforms) => {
- this.platforms = platforms;
+ this.platformSubscription = this.platformService.getPlatforms().subscribe({
+ next: (platforms: Platform[]) => {
+ this.platforms = this.normalizeIds(platforms, 'id');
+ this.filteredPlatforms = [...this.platforms];
},
error: (error) => {
console.error('Error fetching platforms:', error);
@@ -140,21 +203,118 @@ export class AddProductComponent implements OnInit, OnDestroy {
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 {
this.addProductSubscription?.unsubscribe();
+ this.brandControlSubscription?.unsubscribe();
+ this.platformControlSubscription?.unsubscribe();
+ this.brandSubscription?.unsubscribe();
+ this.platformSubscription?.unsubscribe();
+ this.categorySubscription?.unsubscribe();
+ this.conditionSubscription?.unsubscribe();
}
onProductAdd() {
-
this.isSubmitted = true;
if (this.addProductForm.valid) {
this.isLoading = true;
- const productData = this.addProductForm.value;
- alert("Produit ajouté avec succès !");
- console.log(productData);
+ 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 !");
+ },
+ 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 '';
}
+
+ 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);
+ };
}
diff --git a/client/src/app/pages/products/products.component.css b/client/src/app/pages/products/products.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/app/pages/products/products.component.html b/client/src/app/pages/products/products.component.html
new file mode 100644
index 0000000..ff56460
--- /dev/null
+++ b/client/src/app/pages/products/products.component.html
@@ -0,0 +1 @@
+
diff --git a/client/src/app/pages/products/products.component.ts b/client/src/app/pages/products/products.component.ts
new file mode 100644
index 0000000..d2cf933
--- /dev/null
+++ b/client/src/app/pages/products/products.component.ts
@@ -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 {
+
+}
diff --git a/client/src/app/services/brand/brand.service.ts b/client/src/app/services/app/brand.service.ts
similarity index 92%
rename from client/src/app/services/brand/brand.service.ts
rename to client/src/app/services/app/brand.service.ts
index 2c43e9c..e7801aa 100644
--- a/client/src/app/services/brand/brand.service.ts
+++ b/client/src/app/services/app/brand.service.ts
@@ -8,7 +8,7 @@ import {Brand} from '../../interfaces/brand';
export class BrandService {
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() {
return this.http.get(this.BASE_URL, {withCredentials: true});
diff --git a/client/src/app/services/app/category.service.ts b/client/src/app/services/app/category.service.ts
new file mode 100644
index 0000000..0c47cb3
--- /dev/null
+++ b/client/src/app/services/app/category.service.ts
@@ -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(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});
+ }
+}
diff --git a/client/src/app/services/app/condition.service.ts b/client/src/app/services/app/condition.service.ts
new file mode 100644
index 0000000..f4eaa3b
--- /dev/null
+++ b/client/src/app/services/app/condition.service.ts
@@ -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(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});
+ }
+}
diff --git a/client/src/app/services/platform/platform.service.ts b/client/src/app/services/app/platform.service.ts
similarity index 92%
rename from client/src/app/services/platform/platform.service.ts
rename to client/src/app/services/app/platform.service.ts
index b717eac..8147153 100644
--- a/client/src/app/services/platform/platform.service.ts
+++ b/client/src/app/services/app/platform.service.ts
@@ -8,7 +8,7 @@ import {Platform} from '../../interfaces/platform';
export class PlatformService {
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() {
return this.http.get(this.BASE_URL, {withCredentials: true});
diff --git a/client/src/app/services/app/product.service.ts b/client/src/app/services/app/product.service.ts
new file mode 100644
index 0000000..56f64ea
--- /dev/null
+++ b/client/src/app/services/app/product.service.ts
@@ -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(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});
+ }
+}
diff --git a/client/src/app/services/product/product.service.ts b/client/src/app/services/product/product.service.ts
deleted file mode 100644
index af21b65..0000000
--- a/client/src/app/services/product/product.service.ts
+++ /dev/null
@@ -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() { }
-}