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 { MatDialogRef, MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogTitle } from '@angular/material/dialog'; import { MatIcon } from '@angular/material/icon'; 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'; export type ProductDialogData = { mode: 'create' | 'edit'; refs: { categories: PsItem[]; manufacturers: PsItem[]; suppliers: PsItem[]; }; productRow?: ProductListItem & { priceHt?: number }; }; type CarouselItem = { src: string; isPlaceholder: boolean }; @Component({ selector: 'app-ps-product-dialog', standalone: true, templateUrl: './ps-product-dialog.component.html', styleUrls: ['./ps-product-dialog.component.css'], imports: [ CommonModule, ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelectModule, MatCheckbox, MatButton, MatDialogActions, MatDialogContent, MatDialogTitle, MatIcon, MatIconButton ] }) export class PsProductDialogComponent implements OnInit, OnDestroy { private readonly fb = inject(FormBuilder); private readonly ps = inject(PrestashopService); constructor( @Inject(MAT_DIALOG_DATA) public data: ProductDialogData, private readonly dialogRef: MatDialogRef ) {} mode!: 'create' | 'edit'; categories: PsItem[] = []; manufacturers: PsItem[] = []; suppliers: PsItem[] = []; productRow?: ProductListItem & { priceHt?: number }; images: File[] = []; existingImageUrls: string[] = []; // Previews des fichiers nouvellement sélectionnés previewUrls: string[] = []; // items du carrousel (existants + nouveaux + placeholder) carouselItems: CarouselItem[] = []; currentIndex = 0; // options possibles pour l'état (Neuf, Très bon état, etc.) conditionOptions: string[] = []; // on conserve la dernière description chargée pour éviter l’écrasement à vide private lastLoadedDescription = ''; form = this.fb.group({ name: ['', Validators.required], description: [''], categoryId: [null as number | null, Validators.required], manufacturerId: [null as number | null, Validators.required], supplierId: [null as number | null, Validators.required], complete: [true], hasManual: [true], conditionLabel: ['', Validators.required], priceTtc: [0, [Validators.required, Validators.min(0)]], quantity: [0, [Validators.required, Validators.min(0)]], }); // ---------- Helpers locaux ---------- private toTtc(ht: number) { return Math.round(((ht * 1.2) + Number.EPSILON) * 100) / 100; } /** normalisation simple pour comparaison de labels (insensible à casse/accents) */ private normalizeLabel(s: string): string { return String(s ?? '') .normalize('NFD') .replaceAll(/[\u0300-\u036f]/g, '') .toLowerCase() .trim(); } /** enlève si présent */ private stripCdata(s: string): string { if (!s) return ''; return s.startsWith('') ? s.slice(9, -3) : s; } /** convertit du HTML en texte (pour le textarea) */ private htmlToText(html: string): string { if (!html) return ''; const div = document.createElement('div'); div.innerHTML = html; return (div.textContent || div.innerText || '').trim(); } /** nettoyage CDATA+HTML -> texte simple */ private cleanForTextarea(src: string): string { return this.htmlToText(this.stripCdata(src ?? '')); } ngOnInit(): void { this.mode = this.data.mode; this.productRow = this.data.productRow; // Les refs viennent déjà triées du service const rawCategories = this.data.refs.categories ?? []; const rawManufacturers = this.data.refs.manufacturers ?? []; const rawSuppliers = this.data.refs.suppliers ?? []; const forbiddenCats = new Set(['racine', 'root', 'accueil', 'home']); // on filtre seulement ici, tri déjà fait côté service this.categories = rawCategories.filter( c => !forbiddenCats.has(this.normalizeLabel(c.name)) ); this.manufacturers = rawManufacturers; this.suppliers = rawSuppliers; // charger les valeurs possibles pour l’état (Neuf, Très bon état, etc.) this.ps.getConditionValues() .pipe(catchError(() => of([]))) .subscribe((opts: string[]) => this.conditionOptions = opts); // ---- Mode édition : pré-remplissage ---- if (this.mode === 'edit' && this.productRow) { const r = this.productRow; const immediateTtc = r.priceHt == null ? 0 : this.toTtc(r.priceHt); this.form.patchValue({ name: r.name, categoryId: r.id_category_default ?? null, manufacturerId: r.id_manufacturer ?? null, supplierId: r.id_supplier ?? null, priceTtc: immediateTtc }); const details$ = this.ps.getProductDetails(r.id).pipe( catchError(() => of({ id: r.id, name: r.name, description: '', id_manufacturer: r.id_manufacturer, id_supplier: r.id_supplier, id_category_default: r.id_category_default, priceHt: r.priceHt ?? 0 })) ); 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 })) ); 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; this.form.patchValue({ description: baseDesc, complete: flags.complete, hasManual: flags.hasManual, conditionLabel: flags.conditionLabel || '', priceTtc: (ttc || this.form.value.priceTtc || 0), quantity: qty, categoryId: (details.id_category_default ?? this.form.value.categoryId) ?? null, manufacturerId: (details.id_manufacturer ?? this.form.value.manufacturerId) ?? null, supplierId: (details.id_supplier ?? this.form.value.supplierId) ?? null }); this.existingImageUrls = imgs; this.buildCarousel(); }); } else { // mode création : uniquement placeholder au début this.buildCarousel(); } } // -------- Carrousel / gestion des fichiers -------- onFiles(ev: Event) { const fl = (ev.target as HTMLInputElement).files; // Nettoyage des anciens objectURL for(let url of this.previewUrls) { URL.revokeObjectURL(url); } this.previewUrls = []; this.images = []; if (fl) { this.images = Array.from(fl); this.previewUrls = this.images.map(f => URL.createObjectURL(f)); } this.buildCarousel(); // si on a ajouté des images, se placer sur la première nouvelle if (this.images.length && this.existingImageUrls.length + this.previewUrls.length > 0) { this.currentIndex = this.existingImageUrls.length; // première nouvelle } } private buildCarousel() { const items: CarouselItem[] = [ ...this.existingImageUrls.map(u => ({ src: u, isPlaceholder: false })), ...this.previewUrls.map(u => ({ src: u, isPlaceholder: false })) ]; // placeholder en dernier items.push({ src: '', isPlaceholder: true }); this.carouselItems = items; if (!this.carouselItems.length) { this.currentIndex = 0; } else if (this.currentIndex >= this.carouselItems.length) { this.currentIndex = 0; } } prev() { if (!this.carouselItems.length) return; this.currentIndex = (this.currentIndex - 1 + this.carouselItems.length) % this.carouselItems.length; } next() { if (!this.carouselItems.length) return; this.currentIndex = (this.currentIndex + 1) % this.carouselItems.length; } onThumbClick(index: number) { if (index < 0 || index >= this.carouselItems.length) return; this.currentIndex = index; } ngOnDestroy() { // Nettoyage des objectURL for (let url of this.previewUrls) { URL.revokeObjectURL(url); } } // -------- Save / close inchangés (à part dto.images) -------- save() { if (this.form.invalid) return; const v = this.form.getRawValue(); const effectiveDescription = (v.description ?? '').trim() || this.lastLoadedDescription; const dto = { name: v.name!, description: effectiveDescription, categoryId: +v.categoryId!, manufacturerId: +v.manufacturerId!, supplierId: +v.supplierId!, images: this.images, // toujours les fichiers sélectionnés complete: !!v.complete, hasManual: !!v.hasManual, conditionLabel: v.conditionLabel || undefined, priceTtc: Number(v.priceTtc ?? 0), vatRate: 0.2, quantity: Math.max(0, Number(v.quantity ?? 0)) }; let op$: Observable; if (this.mode === 'create' || !this.productRow) { op$ = this.ps.createProduct(dto) as Observable; } else { op$ = this.ps.updateProduct(this.productRow.id, dto) as Observable; } op$.subscribe({ next: () => this.dialogRef.close(true), error: (e: unknown) => alert('Erreur: ' + (e instanceof Error ? e.message : String(e))) }); } close() { this.dialogRef.close(false); } }