From 1a5d3a570ab374398751166865074e08e244f512 Mon Sep 17 00:00:00 2001 From: Vincent Guillet Date: Wed, 3 Dec 2025 21:21:50 +0100 Subject: [PATCH] Add image deletion functionality to product dialog carousel --- .../ps-product-dialog.component.css | 47 ++++++- .../ps-product-dialog.component.html | 25 ++++ .../ps-product-dialog.component.ts | 119 ++++++++++++++---- 3 files changed, 169 insertions(+), 22 deletions(-) diff --git a/client/src/app/components/ps-product-dialog/ps-product-dialog.component.css b/client/src/app/components/ps-product-dialog/ps-product-dialog.component.css index 2532832..e9b466d 100644 --- a/client/src/app/components/ps-product-dialog/ps-product-dialog.component.css +++ b/client/src/app/components/ps-product-dialog/ps-product-dialog.component.css @@ -76,6 +76,24 @@ font-size: 40px; } +/* Bouton de suppression (croix rouge) */ +.carousel-delete-btn { + position: absolute; + top: 6px; + right: 6px; + background: rgba(255, 255, 255, 0.9); + border-radius: 4px; + padding: 2px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); +} + +.carousel-delete-btn mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + color: #e53935; +} + /* Bandeau de vignettes */ .carousel-thumbs { @@ -85,15 +103,42 @@ } .thumb-item { + position: relative; width: 64px; height: 64px; border-radius: 4px; - overflow: hidden; + overflow: hidden; /* tu peux laisser comme ça */ border: 2px solid transparent; flex: 0 0 auto; cursor: pointer; } +/* Bouton de suppression sur les vignettes */ +.thumb-delete-btn { + position: absolute; + top: 2px; + right: 2px; + + width: 18px; + height: 18px; + min-width: 18px; + padding: 0; + + line-height: 18px; + + background: transparent; + border-radius: 0; + box-shadow: none; +} + +.thumb-delete-btn mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + + color: #e53935; /* rouge discret mais lisible */ +} + .thumb-item.active { border-color: #1976d2; } diff --git a/client/src/app/components/ps-product-dialog/ps-product-dialog.component.html b/client/src/app/components/ps-product-dialog/ps-product-dialog.component.html index a71ddb5..dec97b9 100644 --- a/client/src/app/components/ps-product-dialog/ps-product-dialog.component.html +++ b/client/src/app/components/ps-product-dialog/ps-product-dialog.component.html @@ -31,6 +31,23 @@ [disabled]="carouselItems.length <= 1"> chevron_right + + + @if (carouselItems.length && !carouselItems[currentIndex].isPlaceholder) { + + } + + + @@ -40,6 +57,14 @@ [class.active]="i === currentIndex" (click)="onThumbClick(i)"> @if (!item.isPlaceholder) { + + + + Vignette produit } @else {
diff --git a/client/src/app/components/ps-product-dialog/ps-product-dialog.component.ts b/client/src/app/components/ps-product-dialog/ps-product-dialog.component.ts index 15f7610..eb47b52 100644 --- a/client/src/app/components/ps-product-dialog/ps-product-dialog.component.ts +++ b/client/src/app/components/ps-product-dialog/ps-product-dialog.component.ts @@ -1,11 +1,11 @@ import {Component, Inject, OnInit, inject, OnDestroy} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { MatFormField, MatLabel } from '@angular/material/form-field'; -import { MatInput } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { MatButton, MatIconButton } from '@angular/material/button'; +import {CommonModule} from '@angular/common'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {MatFormField, MatLabel} from '@angular/material/form-field'; +import {MatInput} from '@angular/material/input'; +import {MatSelectModule} from '@angular/material/select'; +import {MatCheckbox} from '@angular/material/checkbox'; +import {MatButton, MatIconButton} from '@angular/material/button'; import { MatDialogRef, MAT_DIALOG_DATA, @@ -13,13 +13,13 @@ import { MatDialogContent, MatDialogTitle } from '@angular/material/dialog'; -import { MatIcon } from '@angular/material/icon'; +import {MatIcon} from '@angular/material/icon'; -import { catchError, forkJoin, of, Observable } from 'rxjs'; +import {catchError, forkJoin, of, Observable} from 'rxjs'; -import { PsItem } from '../../interfaces/ps-item'; -import { ProductListItem } from '../../interfaces/product-list-item'; -import { PrestashopService } from '../../services/prestashop.serivce'; +import {PsItem} from '../../interfaces/ps-item'; +import {ProductListItem} from '../../interfaces/product-list-item'; +import {PrestashopService} from '../../services/prestashop.serivce'; export type ProductDialogData = { mode: 'create' | 'edit'; @@ -48,7 +48,8 @@ export class PsProductDialogComponent implements OnInit, OnDestroy { constructor( @Inject(MAT_DIALOG_DATA) public data: ProductDialogData, private readonly dialogRef: MatDialogRef - ) {} + ) { + } mode!: 'create' | 'edit'; categories: PsItem[] = []; @@ -167,11 +168,11 @@ export class PsProductDialogComponent implements OnInit, OnDestroy { const qty$ = this.ps.getProductQuantity(r.id).pipe(catchError(() => of(0))); const imgs$ = this.ps.getProductImageUrls(r.id).pipe(catchError(() => of([]))); const flags$ = this.ps.getProductFlags(r.id).pipe( - catchError(() => of({ complete: false, hasManual: false, conditionLabel: undefined })) + catchError(() => of({complete: false, hasManual: false, conditionLabel: undefined})) ); - forkJoin({ details: details$, qty: qty$, imgs: imgs$, flags: flags$ }) - .subscribe(({ details, qty, imgs, flags }) => { + forkJoin({details: details$, qty: qty$, imgs: imgs$, flags: flags$}) + .subscribe(({details, qty, imgs, flags}) => { const ttc = this.toTtc(details.priceHt ?? 0); const baseDesc = this.cleanForTextarea(details.description ?? ''); this.lastLoadedDescription = baseDesc; @@ -203,7 +204,7 @@ export class PsProductDialogComponent implements OnInit, OnDestroy { const fl = (ev.target as HTMLInputElement).files; // Nettoyage des anciens objectURL - for(let url of this.previewUrls) { + for (let url of this.previewUrls) { URL.revokeObjectURL(url); } this.previewUrls = []; @@ -224,12 +225,12 @@ export class PsProductDialogComponent implements OnInit, OnDestroy { private buildCarousel() { const items: CarouselItem[] = [ - ...this.existingImageUrls.map(u => ({ src: u, isPlaceholder: false })), - ...this.previewUrls.map(u => ({ src: u, isPlaceholder: false })) + ...this.existingImageUrls.map(u => ({src: u, isPlaceholder: false})), + ...this.previewUrls.map(u => ({src: u, isPlaceholder: false})) ]; // placeholder en dernier - items.push({ src: '', isPlaceholder: true }); + items.push({src: '', isPlaceholder: true}); this.carouselItems = items; if (!this.carouselItems.length) { @@ -261,7 +262,7 @@ export class PsProductDialogComponent implements OnInit, OnDestroy { } } - // -------- Save / close inchangés (à part dto.images) -------- + // -------- Save / close -------- save() { if (this.form.invalid) return; @@ -297,6 +298,82 @@ export class PsProductDialogComponent implements OnInit, OnDestroy { }); } + /** Extrait l'id_image depuis une URL FO Presta (.../img/p/.../.jpg) */ + private extractImageIdFromUrl(url: string): number | null { + const m = /\/(\d+)\.(?:jpg|jpeg|png|gif)$/i.exec(url); + if (!m) return null; + const id = Number(m[1]); + return Number.isFinite(id) ? id : null; + } + + /** Suppression générique d'une image à l'index donné (carrousel + vignettes) */ + private deleteImageAtIndex(idx: number) { + if (!this.carouselItems.length) return; + + const item = this.carouselItems[idx]; + if (!item || item.isPlaceholder) return; + + const existingCount = this.existingImageUrls.length; + + // --- Cas 1 : image existante (déjà chez Presta) --- + if (idx < existingCount) { + if (!this.productRow) return; // sécurité + + const url = this.existingImageUrls[idx]; + const imageId = this.extractImageIdFromUrl(url); + if (!imageId) { + alert('Impossible de déterminer l’ID de l’image à supprimer.'); + return; + } + + if (!confirm('Supprimer cette image du produit ?')) return; + + this.ps.deleteProductImage(this.productRow.id, imageId).subscribe({ + next: () => { + // On la retire du tableau local et on reconstruit le carrousel + this.existingImageUrls.splice(idx, 1); + this.buildCarousel(); + + // Repositionnement de l’index si nécessaire + if (this.currentIndex >= this.carouselItems.length - 1) { + this.currentIndex = Math.max(0, this.carouselItems.length - 2); + } + }, + error: (e: unknown) => { + alert('Erreur lors de la suppression de l’image : ' + (e instanceof Error ? e.message : String(e))); + } + }); + + return; + } + + // --- Cas 2 : image locale (nouvelle) --- + const localIdx = idx - existingCount; + if (localIdx >= 0 && localIdx < this.previewUrls.length) { + if (!confirm('Retirer cette image de la sélection ?')) return; + + this.previewUrls.splice(localIdx, 1); + this.images.splice(localIdx, 1); + this.buildCarousel(); + + if (this.currentIndex >= this.carouselItems.length - 1) { + this.currentIndex = Math.max(0, this.carouselItems.length - 2); + } + } + } + + // utilisée par la grande image + onDeleteCurrentImage() { + if (!this.carouselItems.length) return; + this.deleteImageAtIndex(this.currentIndex); + } + + // utilisée par la croix sur une vignette + onDeleteThumb(index: number, event: MouseEvent) { + event.stopPropagation(); + this.deleteImageAtIndex(index); + } + close() { this.dialogRef.close(false); }