From a849a4dd1593aeb3315e8cc5269fd200ddcc7673 Mon Sep 17 00:00:00 2001 From: Vincent Guillet Date: Mon, 10 Nov 2025 16:29:42 +0100 Subject: [PATCH] feat: add categories and manufacturers CRUD components; implement form handling and image upload functionality --- client/proxy.conf.json | 12 + .../app/admin-presta/admin-presta.module.ts | 32 +++ .../categories-crud.component.css | 18 ++ .../categories-crud.component.html | 36 +++ .../categories-crud.component.ts | 99 ++++++++ .../manufacturers-crud.component.css | 18 ++ .../manufacturers-crud.component.html | 40 +++ .../manufacturers-crud.component.ts | 99 ++++++++ .../ps-crud-tabs/ps-crud-tabs.component.css | 5 + .../ps-crud-tabs/ps-crud-tabs.component.html | 8 + .../ps-crud-tabs/ps-crud-tabs.component.ts | 20 ++ .../suppliers-crud.component.css | 18 ++ .../suppliers-crud.component.html | 40 +++ .../suppliers-crud.component.ts | 99 ++++++++ client/src/app/app.routes.ts | 7 + .../main-navbar/main-navbar.component.html | 4 + client/src/app/interfaces/image.ts | 5 + .../add-product/add-product.component.html | 13 +- .../add-product/add-product.component.ts | 158 ++++++++---- client/src/app/services/app/image.service.ts | 17 ++ .../services/app/product_images.service.ts | 33 +++ client/src/app/services/presta.serivce.ts | 234 ++++++++++++++++++ 22 files changed, 968 insertions(+), 47 deletions(-) create mode 100644 client/proxy.conf.json create mode 100644 client/src/app/admin-presta/admin-presta.module.ts create mode 100644 client/src/app/admin-presta/categories-crud/categories-crud.component.css create mode 100644 client/src/app/admin-presta/categories-crud/categories-crud.component.html create mode 100644 client/src/app/admin-presta/categories-crud/categories-crud.component.ts create mode 100644 client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.css create mode 100644 client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.html create mode 100644 client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.ts create mode 100644 client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.css create mode 100644 client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.html create mode 100644 client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.ts create mode 100644 client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.css create mode 100644 client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.html create mode 100644 client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.ts create mode 100644 client/src/app/interfaces/image.ts create mode 100644 client/src/app/services/app/image.service.ts create mode 100644 client/src/app/services/app/product_images.service.ts create mode 100644 client/src/app/services/presta.serivce.ts diff --git a/client/proxy.conf.json b/client/proxy.conf.json new file mode 100644 index 0000000..3c46c51 --- /dev/null +++ b/client/proxy.conf.json @@ -0,0 +1,12 @@ +{ + "/ps": { + "target": "https://shop.gameovergne.fr", + "secure": true, + "changeOrigin": true, + "logLevel": "debug", + "pathRewrite": { "^/ps": "/api" }, + "headers": { + "Authorization": "Basic MkFRUEcxM01KOFgxMTdVNkZKNU5HSFBTOTNIRTM0QUI=" + } + } +} diff --git a/client/src/app/admin-presta/admin-presta.module.ts b/client/src/app/admin-presta/admin-presta.module.ts new file mode 100644 index 0000000..61d2b82 --- /dev/null +++ b/client/src/app/admin-presta/admin-presta.module.ts @@ -0,0 +1,32 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ReactiveFormsModule, FormsModule} from '@angular/forms'; + +// Angular Material (tous utilisés dans ces composants) +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTableModule} from '@angular/material/table'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; + +// Composants de cette feature +import {PsCrudTabsComponent} from './ps-crud-tabs/ps-crud-tabs.component'; +import {CategoriesCrudComponent} from './categories-crud/categories-crud.component'; +import {ManufacturersCrudComponent} from './manufacturers-crud/manufacturers-crud.component'; +import {SuppliersCrudComponent} from './suppliers-crud/suppliers-crud.component'; + +@NgModule({ + declarations: [ + + ], + imports: [ + CommonModule, + ReactiveFormsModule, FormsModule, + MatTabsModule, MatTableModule, MatFormFieldModule, MatInputModule, + MatButtonModule, MatIconModule, PsCrudTabsComponent, CategoriesCrudComponent, ManufacturersCrudComponent, SuppliersCrudComponent + ], + exports: [PsCrudTabsComponent] // pour pouvoir l’utiliser ailleurs +}) +export class AdminPrestaModule { +} diff --git a/client/src/app/admin-presta/categories-crud/categories-crud.component.css b/client/src/app/admin-presta/categories-crud/categories-crud.component.css new file mode 100644 index 0000000..a6fe2c5 --- /dev/null +++ b/client/src/app/admin-presta/categories-crud/categories-crud.component.css @@ -0,0 +1,18 @@ +.crud { + display: grid; + gap: 16px +} + +.row { + display: flex; + gap: 12px; + align-items: flex-end +} + +.grow { + flex: 1 +} + +table { + width: 100% +} diff --git a/client/src/app/admin-presta/categories-crud/categories-crud.component.html b/client/src/app/admin-presta/categories-crud/categories-crud.component.html new file mode 100644 index 0000000..542d81f --- /dev/null +++ b/client/src/app/admin-presta/categories-crud/categories-crud.component.html @@ -0,0 +1,36 @@ +
+
+ + Nom de la catégorie + + + + + +
+ + + + + + + + + + + + + + + + + + + +
ID{{ el.id }}Nom{{ el.name }}Actions + + +
+
diff --git a/client/src/app/admin-presta/categories-crud/categories-crud.component.ts b/client/src/app/admin-presta/categories-crud/categories-crud.component.ts new file mode 100644 index 0000000..e783492 --- /dev/null +++ b/client/src/app/admin-presta/categories-crud/categories-crud.component.ts @@ -0,0 +1,99 @@ +import {Component, inject, OnInit} from '@angular/core'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {PrestaService, PsItem} from '../../services/presta.serivce'; +import {map} from 'rxjs'; +import {MatIcon} from '@angular/material/icon'; +import {MatButton, MatIconButton} from '@angular/material/button'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatTable +} from '@angular/material/table'; +import {MatFormField, MatLabel} from '@angular/material/form-field'; +import {MatInput} from '@angular/material/input'; +import {NgIf} from '@angular/common'; + +@Component({ + standalone : true, + selector: 'app-categories-crud', + templateUrl: './categories-crud.component.html', + styleUrls: ['./categories-crud.component.css'], + imports: [ + MatIcon, + MatIconButton, + MatHeaderRow, + MatRow, + MatRowDef, + MatHeaderRowDef, + ReactiveFormsModule, + MatFormField, + MatLabel, + MatInput, + MatButton, + MatTable, + MatColumnDef, + MatHeaderCell, + NgIf, + MatHeaderCellDef, + MatCellDef, + MatCell + ] +}) +export class CategoriesCrudComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly ps = inject(PrestaService); + + items: PsItem[] = []; + cols = ['id', 'name', 'actions']; + form = this.fb.group({name: ['', Validators.required]}); + editId: number | null = null; + + ngOnInit() { + this.reload(); + } + + reload() { + this.ps.list('categories').subscribe(data => this.items = data); + } + + startEdit(el: PsItem) { + this.editId = el.id; + this.form.patchValue({name: el.name}); + } + + cancelEdit() { + this.editId = null; + this.form.reset({name: ''}); + } + + onSubmit() { + const name = this.form.value.name!.trim(); + if (!name) return; + + const op$ = this.editId + ? this.ps.update('categories', this.editId, name).pipe(map(() => undefined)) + : this.ps.create('categories', name).pipe(map(() => undefined)); + + op$.subscribe({ + next: () => { + this.cancelEdit(); + this.reload(); + }, + error: (e: unknown) => alert('Erreur: ' + (e instanceof Error ? e.message : String(e))) + }); + } + + remove(el: PsItem) { + if (!confirm(`Supprimer la catégorie "${el.name}" (#${el.id}) ?`)) return; + this.ps.delete('categories', el.id).subscribe({ + next: () => this.reload(), + error: e => alert('Erreur: ' + (e?.message || e)) + }); + } +} diff --git a/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.css b/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.css new file mode 100644 index 0000000..a6fe2c5 --- /dev/null +++ b/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.css @@ -0,0 +1,18 @@ +.crud { + display: grid; + gap: 16px +} + +.row { + display: flex; + gap: 12px; + align-items: flex-end +} + +.grow { + flex: 1 +} + +table { + width: 100% +} diff --git a/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.html b/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.html new file mode 100644 index 0000000..1b4beff --- /dev/null +++ b/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.html @@ -0,0 +1,40 @@ +
+
+ + Nom de la marque + + + + + +
+ + + + + + + + + + + + + + + + + + + +
ID{{ el.id }}Nom{{ el.name }}Actions + + +
+
diff --git a/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.ts b/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.ts new file mode 100644 index 0000000..658c661 --- /dev/null +++ b/client/src/app/admin-presta/manufacturers-crud/manufacturers-crud.component.ts @@ -0,0 +1,99 @@ +import {Component, inject, OnInit} from '@angular/core'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {PrestaService, PsItem} from '../../services/presta.serivce'; +import {map} from 'rxjs'; +import {MatFormField, MatLabel} from '@angular/material/form-field'; +import {MatIcon} from '@angular/material/icon'; +import {MatButton, MatIconButton} from '@angular/material/button'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatTable +} from '@angular/material/table'; +import {MatInput} from '@angular/material/input'; +import {NgIf} from '@angular/common'; + +@Component({ + standalone: true, + selector: 'app-manufacturers-crud', + templateUrl: './manufacturers-crud.component.html', + imports: [ + MatIcon, + MatIconButton, + MatHeaderRow, + MatRow, + MatRowDef, + MatHeaderRowDef, + ReactiveFormsModule, + MatFormField, + MatLabel, + MatInput, + MatButton, + MatTable, + MatColumnDef, + MatHeaderCell, + NgIf, + MatHeaderCellDef, + MatCellDef, + MatCell + ], + styleUrls: ['./manufacturers-crud.component.css'] +}) +export class ManufacturersCrudComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly ps = inject(PrestaService); + + items: PsItem[] = []; + cols = ['id', 'name', 'actions']; + form = this.fb.group({name: ['', Validators.required]}); + editId: number | null = null; + + ngOnInit() { + this.reload(); + } + + reload() { + this.ps.list('manufacturers').subscribe(d => this.items = d); + } + + startEdit(el: PsItem) { + this.editId = el.id; + this.form.patchValue({name: el.name}); + } + + cancelEdit() { + this.editId = null; + this.form.reset({name: ''}); + } + + onSubmit() { + const name = this.form.value.name!.trim(); + if (!name) return; + + const op$ = this.editId + ? this.ps.update('manufacturers', this.editId, name).pipe(map(() => undefined)) + : this.ps.create('manufacturers', name).pipe(map(() => undefined)); + + op$.subscribe({ + next: () => { + this.cancelEdit(); + this.reload(); + }, + error: (e: unknown) => alert('Erreur: ' + (e instanceof Error ? e.message : String(e))) + }); + } + + remove(el: PsItem) { + if (!confirm(`Supprimer la marque "${el.name}" (#${el.id}) ?`)) return; + this.ps.delete('manufacturers', el.id).subscribe({ + next: () => this.reload(), + error: e => alert('Erreur: ' + (e?.message || e)) + }); + } +} diff --git a/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.css b/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.css new file mode 100644 index 0000000..239d9d3 --- /dev/null +++ b/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.css @@ -0,0 +1,5 @@ +.wrap { + padding: 16px; + max-width: 900px; + margin: auto +} diff --git a/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.html b/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.html new file mode 100644 index 0000000..017ddcf --- /dev/null +++ b/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.html @@ -0,0 +1,8 @@ +
+

PrestaShop — CRUD simplifié

+ + + + + +
diff --git a/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.ts b/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.ts new file mode 100644 index 0000000..a80b0fd --- /dev/null +++ b/client/src/app/admin-presta/ps-crud-tabs/ps-crud-tabs.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import {MatTab, MatTabGroup} from '@angular/material/tabs'; +import {CategoriesCrudComponent} from '../categories-crud/categories-crud.component'; +import {ManufacturersCrudComponent} from '../manufacturers-crud/manufacturers-crud.component'; +import {SuppliersCrudComponent} from '../suppliers-crud/suppliers-crud.component'; + +@Component({ + standalone: true, + selector: 'app-ps-crud-tabs', + templateUrl: './ps-crud-tabs.component.html', + imports: [ + MatTabGroup, + MatTab, + CategoriesCrudComponent, + ManufacturersCrudComponent, + SuppliersCrudComponent + ], + styleUrls: ['./ps-crud-tabs.component.css'] +}) +export class PsCrudTabsComponent {} diff --git a/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.css b/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.css new file mode 100644 index 0000000..a6fe2c5 --- /dev/null +++ b/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.css @@ -0,0 +1,18 @@ +.crud { + display: grid; + gap: 16px +} + +.row { + display: flex; + gap: 12px; + align-items: flex-end +} + +.grow { + flex: 1 +} + +table { + width: 100% +} diff --git a/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.html b/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.html new file mode 100644 index 0000000..e6118f6 --- /dev/null +++ b/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.html @@ -0,0 +1,40 @@ +
+
+ + Nom du fournisseur + + + + + +
+ + + + + + + + + + + + + + + + + + + +
ID{{ el.id }}Nom{{ el.name }}Actions + + +
+
diff --git a/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.ts b/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.ts new file mode 100644 index 0000000..f9364a1 --- /dev/null +++ b/client/src/app/admin-presta/suppliers-crud/suppliers-crud.component.ts @@ -0,0 +1,99 @@ +import {Component, inject, OnInit} from '@angular/core'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {PrestaService, PsItem} from '../../services/presta.serivce'; +import {map} from 'rxjs'; +import {MatIcon} from '@angular/material/icon'; +import {MatButton, MatIconButton} from '@angular/material/button'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatTable +} from '@angular/material/table'; +import {MatFormField, MatLabel} from '@angular/material/form-field'; +import {MatInput} from '@angular/material/input'; +import {NgIf} from '@angular/common'; + +@Component({ + standalone: true, + selector: 'app-suppliers-crud', + templateUrl: './suppliers-crud.component.html', + imports: [ + MatIcon, + MatIconButton, + MatHeaderRow, + MatRow, + MatRowDef, + MatHeaderRowDef, + ReactiveFormsModule, + MatFormField, + MatLabel, + MatInput, + MatButton, + MatTable, + MatColumnDef, + MatHeaderCell, + NgIf, + MatHeaderCellDef, + MatCellDef, + MatCell + ], + styleUrls: ['./suppliers-crud.component.css'] +}) +export class SuppliersCrudComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly ps = inject(PrestaService); + + items: PsItem[] = []; + cols = ['id', 'name', 'actions']; + form = this.fb.group({name: ['', Validators.required]}); + editId: number | null = null; + + ngOnInit() { + this.reload(); + } + + reload() { + this.ps.list('suppliers').subscribe(d => this.items = d); + } + + startEdit(el: PsItem) { + this.editId = el.id; + this.form.patchValue({name: el.name}); + } + + cancelEdit() { + this.editId = null; + this.form.reset({name: ''}); + } + + onSubmit() { + const name = this.form.value.name!.trim(); + if (!name) return; + + const op$ = this.editId + ? this.ps.update('suppliers', this.editId, name).pipe(map(() => undefined)) + : this.ps.create('suppliers', name).pipe(map(() => undefined)); + + op$.subscribe({ + next: () => { + this.cancelEdit(); + this.reload(); + }, + error: (e: unknown) => alert('Erreur: ' + (e instanceof Error ? e.message : String(e))) + }); + } + + remove(el: PsItem) { + if (!confirm(`Supprimer le fournisseur "${el.name}" (#${el.id}) ?`)) return; + this.ps.delete('suppliers', el.id).subscribe({ + next: () => this.reload(), + error: e => alert('Erreur: ' + (e?.message || e)) + }); + } +} diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index 2b0bda3..58db7e1 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -9,6 +9,7 @@ import {AdminComponent} from './pages/admin/admin.component'; import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard'; import {ProductsComponent} from './pages/products/products.component'; import {AddProductComponent} from './pages/add-product/add-product.component'; +import {PsCrudTabsComponent} from './admin-presta/ps-crud-tabs/ps-crud-tabs.component'; export const routes: Routes = [ { @@ -61,6 +62,12 @@ export const routes: Routes = [ canMatch: [authOnlyCanMatch], canActivate: [authOnlyCanActivate], }, + { + path: 'prestashop', + component: PsCrudTabsComponent, + canMatch: [authOnlyCanMatch], + canActivate: [authOnlyCanActivate], + }, { path: '**', redirectTo: '' diff --git a/client/src/app/components/navbar/main-navbar/main-navbar.component.html b/client/src/app/components/navbar/main-navbar/main-navbar.component.html index bf5757c..3744d3f 100644 --- a/client/src/app/components/navbar/main-navbar/main-navbar.component.html +++ b/client/src/app/components/navbar/main-navbar/main-navbar.component.html @@ -10,6 +10,10 @@ @if (authService.hasRole('Administrator')) { +