feat: add quantity field to product CRUD; implement image upload and condition handling in product dialog
This commit is contained in:
@@ -18,7 +18,7 @@ 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 { PrestashopService } from '../../services/prestashop.serivce';
|
||||
|
||||
export type ProductDialogData = {
|
||||
mode: 'create' | 'edit';
|
||||
@@ -55,6 +55,9 @@ export class PsProductDialogComponent implements OnInit {
|
||||
images: File[] = [];
|
||||
existingImageUrls: string[] = [];
|
||||
|
||||
// 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 = '';
|
||||
|
||||
@@ -64,19 +67,36 @@ export class PsProductDialogComponent implements OnInit {
|
||||
categoryId: [null as number | null, Validators.required],
|
||||
manufacturerId: [null as number | null, Validators.required],
|
||||
supplierId: [null as number | null, Validators.required],
|
||||
complete: [false],
|
||||
hasManual: [false],
|
||||
complete: [true],
|
||||
hasManual: [true],
|
||||
conditionLabel: ['', Validators.required],
|
||||
priceTtc: [0, [Validators.required, Validators.min(0)]],
|
||||
quantity: [0, [Validators.required, Validators.min(0)]],
|
||||
});
|
||||
|
||||
private toTtc(ht: number) { return Math.round(((ht * 1.2) + Number.EPSILON) * 100) / 100; }
|
||||
// ---------- 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 <![CDATA[ ... ]]> si présent */
|
||||
private stripCdata(s: string): string {
|
||||
if (!s) return '';
|
||||
return s.startsWith('<![CDATA[') && s.endsWith(']]>') ? s.slice(9, -3) : s;
|
||||
return s.startsWith('<![CDATA[') && s.endsWith(']]>')
|
||||
? s.slice(9, -3)
|
||||
: s;
|
||||
}
|
||||
|
||||
/** convertit du HTML en texte (pour le textarea) */
|
||||
private htmlToText(html: string): string {
|
||||
if (!html) return '';
|
||||
@@ -84,32 +104,39 @@ export class PsProductDialogComponent implements OnInit {
|
||||
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 ?? ''));
|
||||
}
|
||||
|
||||
/** sépare la description "contenu" des drapeaux + détecte Complet/Notice */
|
||||
private splitDescriptionFlags(desc: string) {
|
||||
const cleaned = this.cleanForTextarea(desc);
|
||||
const complete = /Complet\s*:\s*Oui/i.test(desc);
|
||||
const hasManual = /Notice\s*:\s*Oui/i.test(desc);
|
||||
const idx = cleaned.indexOf('Complet:');
|
||||
const base = (idx >= 0 ? cleaned.slice(0, idx) : cleaned).trim();
|
||||
return { base, complete, hasManual };
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.mode = this.data.mode;
|
||||
this.categories = this.data.refs.categories ?? [];
|
||||
this.manufacturers = this.data.refs.manufacturers ?? [];
|
||||
this.suppliers = this.data.refs.suppliers ?? [];
|
||||
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<string[]>([])))
|
||||
.subscribe((opts: string[]) => this.conditionOptions = opts);
|
||||
|
||||
// ---- Mode édition : pré-remplissage ----
|
||||
if (this.mode === 'edit' && this.productRow) {
|
||||
const r = this.productRow;
|
||||
|
||||
// patch immédiat depuis la ligne
|
||||
const immediateTtc = r.priceHt == null ? 0 : this.toTtc(r.priceHt);
|
||||
this.form.patchValue({
|
||||
name: r.name,
|
||||
@@ -119,7 +146,6 @@ export class PsProductDialogComponent implements OnInit {
|
||||
priceTtc: immediateTtc
|
||||
});
|
||||
|
||||
// patch final via API (tolérant aux erreurs)
|
||||
const details$ = this.ps.getProductDetails(r.id).pipe(
|
||||
catchError(() => of({
|
||||
id: r.id, name: r.name, description: '',
|
||||
@@ -129,22 +155,28 @@ export class PsProductDialogComponent implements OnInit {
|
||||
);
|
||||
const qty$ = this.ps.getProductQuantity(r.id).pipe(catchError(() => of(0)));
|
||||
const imgs$ = this.ps.getProductImageUrls(r.id).pipe(catchError(() => of<string[]>([])));
|
||||
const flags$ = this.ps.getProductFlags(r.id).pipe(
|
||||
catchError(() => of({ complete: false, hasManual: false, conditionLabel: undefined }))
|
||||
);
|
||||
|
||||
forkJoin({ details: details$, qty: qty$, imgs: imgs$ })
|
||||
.subscribe(({ details, qty, imgs }) => {
|
||||
forkJoin({ details: details$, qty: qty$, imgs: imgs$, flags: flags$ })
|
||||
.subscribe(({ details, qty, imgs, flags }) => {
|
||||
const ttc = this.toTtc(details.priceHt ?? 0);
|
||||
const { base, complete, hasManual } = this.splitDescriptionFlags(details.description ?? '');
|
||||
this.lastLoadedDescription = base;
|
||||
const baseDesc = this.cleanForTextarea(details.description ?? '');
|
||||
this.lastLoadedDescription = baseDesc;
|
||||
|
||||
this.form.patchValue({
|
||||
description: base,
|
||||
complete, hasManual,
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -170,7 +202,7 @@ export class PsProductDialogComponent implements OnInit {
|
||||
images: this.images,
|
||||
complete: !!v.complete,
|
||||
hasManual: !!v.hasManual,
|
||||
conditionLabel: undefined,
|
||||
conditionLabel: v.conditionLabel || undefined,
|
||||
priceTtc: Number(v.priceTtc ?? 0),
|
||||
vatRate: 0.2,
|
||||
quantity: Math.max(0, Number(v.quantity ?? 0))
|
||||
|
||||
Reference in New Issue
Block a user