From d4ffcf05624c55da358a298ec9246caf51844ac2 Mon Sep 17 00:00:00 2001 From: Vincent Guillet Date: Tue, 18 Nov 2025 15:38:24 +0100 Subject: [PATCH] feat: add quantity field to product CRUD; implement image upload and condition handling in product dialog --- .../ps-product-crud.component.html | 5 + .../ps-product-crud.component.ts | 5 +- .../ps-product-dialog.component.html | 63 +- .../ps-product-dialog.component.ts | 86 +- client/src/app/services/prestashop.serivce.ts | 830 ++++++++++++------ 5 files changed, 691 insertions(+), 298 deletions(-) diff --git a/client/src/app/components/ps-product-crud/ps-product-crud.component.html b/client/src/app/components/ps-product-crud/ps-product-crud.component.html index 0dd31e9..1f748ce 100644 --- a/client/src/app/components/ps-product-crud/ps-product-crud.component.html +++ b/client/src/app/components/ps-product-crud/ps-product-crud.component.html @@ -43,6 +43,11 @@ {{ el.priceTtc | number:'1.2-2' }} + + Quantité + {{ el.quantity }} + + Actions diff --git a/client/src/app/components/ps-product-crud/ps-product-crud.component.ts b/client/src/app/components/ps-product-crud/ps-product-crud.component.ts index 2593825..3a7d5fc 100644 --- a/client/src/app/components/ps-product-crud/ps-product-crud.component.ts +++ b/client/src/app/components/ps-product-crud/ps-product-crud.component.ts @@ -50,7 +50,7 @@ export class PsProductCrudComponent implements OnInit { private supMap = new Map(); // table - displayed: string[] = ['id', 'name', 'category', 'manufacturer', 'supplier', 'priceTtc', 'actions']; + displayed: string[] = ['id', 'name', 'category', 'manufacturer', 'supplier', 'priceTtc', 'quantity', 'actions']; dataSource = new MatTableDataSource([]); @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; @@ -85,7 +85,8 @@ export class PsProductCrudComponent implements OnInit { String(row.id).includes(f) || (row.categoryName?.toLowerCase().includes(f)) || (row.manufacturerName?.toLowerCase().includes(f)) || - (row.supplierName?.toLowerCase().includes(f)); + (row.supplierName?.toLowerCase().includes(f)) || + String(row.quantity ?? '').includes(f); } private toTtc(ht: number, vat: number) { 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 9ca0750..574bafe 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 @@ -1,11 +1,37 @@

{{ mode === 'create' ? 'Nouveau produit' : 'Modifier le produit' }}

- + + +
+ + +
+ + + @if (mode==='edit' && existingImageUrls.length) { +
+
+ @for (url of existingImageUrls; track url) { + Produit + } +
+
+ } + + + Nom du produit + + + Description + + + + Catégorie @@ -16,6 +42,17 @@ + + + État + + @for (opt of conditionOptions; track opt) { + {{ opt }} + } + + + + Marque @@ -26,8 +63,9 @@ + - Fournisseur + Plateforme Choisir… @for (s of suppliers; track s.id) { @@ -36,34 +74,19 @@ - - Description - - - +
Complet Notice
-
- - -
- -
-
- @for (url of existingImageUrls; track url) { - Produit - } -
-
- + Prix TTC (€) + Quantité 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 aea6f59..c521b53 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 @@ -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 si présent */ private stripCdata(s: string): string { if (!s) return ''; - return s.startsWith('') ? s.slice(9, -3) : s; + return s.startsWith('') + ? 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([]))) + .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([]))); + 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)) diff --git a/client/src/app/services/prestashop.serivce.ts b/client/src/app/services/prestashop.serivce.ts index ba33c17..bdae8fb 100644 --- a/client/src/app/services/prestashop.serivce.ts +++ b/client/src/app/services/prestashop.serivce.ts @@ -1,6 +1,6 @@ import {inject, Injectable} from '@angular/core'; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; -import {forkJoin, map, of, switchMap, Observable} from 'rxjs'; +import {forkJoin, map, of, switchMap, Observable, catchError} from 'rxjs'; import {PsItem} from '../interfaces/ps-item'; import {PsProduct} from '../interfaces/ps-product'; import {ProductListItem} from '../interfaces/product-list-item'; @@ -19,64 +19,63 @@ const UPDATE_CFG: Record https://.../api + private readonly base = '/ps'; // -------- Utils - /** Id de la catégorie d’accueil (Home) de la boutique: PS_HOME_CATEGORY */ - private getHomeCategoryId() { - const params = new HttpParams() - .set('display', '[value]') - .set('filter[name]', 'PS_HOME_CATEGORY') - .set('output_format', 'JSON'); - return this.http.get(`${this.base}/configurations`, {params}).pipe( - map(r => +r?.configurations?.[0]?.value || 2) // fallback 2 - ); - } + private readonly headersXml = new HttpHeaders({ + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }); - /** Id de la catégorie racine (PS_ROOT_CATEGORY) */ - private getRootCategoryId() { - const params = new HttpParams() - .set('display', '[value]') - .set('filter[name]', 'PS_ROOT_CATEGORY') - .set('output_format', 'JSON'); - return this.http.get(`${this.base}/configurations`, {params}).pipe( - map(r => +r?.configurations?.[0]?.value || 1) // fallback 1 - ); - } + private conditionValuesCache: string[] | null = null; - private readonly headersXml = new HttpHeaders({'Content-Type': 'application/xml', 'Accept': 'application/xml'}); + private normalizeLabel(s: string): string { + return String(s ?? '') + .normalize('NFD') + .replaceAll(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim(); + } private escapeXml(v: string) { return String(v) - .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') - .replaceAll('"', '"').replaceAll("'", '''); + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); } - /** Extrait un y compris avec CDATA et attributs éventuels */ private extractIdFromXml(xml: string): number | null { const s = String(xml); - // 1) prioritaire: id du noeud (avant ) - const mProduct = s.match( - /]*>\s*(?:)?\s*<\/id>[\s\S]*?(?:|<\/product>)/i - ); + // 1) cas le plus sûr : ... (avant ) + const mProduct = /]*>\s*(?:)?\s*<\/id>[\s\S]*?(?:|<\/product>)/i.exec(s); if (mProduct) return +mProduct[1]; - // 2) racine quelconque (tolère CDATA) - const mRoot = s.match( - /]*>[\s\S]*?]*>\s*(?:)?\s*<\/id>/i - ); + // 2) racine quelconque + const mRoot = /]*>[\s\S]*?]*>\s*(?:)?\s*<\/id>/i.exec(s); if (mRoot) return +mRoot[2]; - // 3) fallback: premier numérique - const mAny = s.match(/]*>\s*(?:)?\s*<\/id>/i); + // 3) fallback + const mAny = /]*>\s*(?:)?\s*<\/id>/i.exec(s); if (mAny) return +mAny[1]; console.warn('[Presta] Impossible d’extraire depuis la réponse:', xml); @@ -84,7 +83,8 @@ export class PrestashopService { } private slug(str: string) { - return String(str).toLowerCase().normalize('NFD') + return String(str).toLowerCase() + .normalize('NFD') .replaceAll(/[\u0300-\u036f]/g, '') .replaceAll(/[^a-z0-9]+/g, '-') .replaceAll(/(^-|-$)/g, ''); @@ -98,66 +98,117 @@ export class PrestashopService { } private ensureArrayLang(v: any): Array<{ id: number; value: string }> { - if (Array.isArray(v)) return v.map(x => ({id: +x.id, value: String(x.value ?? '')})); + if (Array.isArray(v)) { + return v.map(x => ({id: +x.id, value: String(x.value ?? '')})); + } return []; } - private round2(n: number) { - return Math.round((n + Number.EPSILON) * 100) / 100; - } - private ttcToHt(priceTtc: number, rate = 0.2) { - return this.round2(priceTtc / (1 + rate)); + const raw = priceTtc / (1 + rate); + return Number(raw.toFixed(6)); } - // -------- Contexte, langues, fiscalité + // -------- Contexte & langues + /** Langue par défaut (PS_LANG_DEFAULT) */ getDefaultLangId() { const params = new HttpParams() .set('display', '[value]') .set('filter[name]', 'PS_LANG_DEFAULT') .set('output_format', 'JSON'); + return this.http.get(`${this.base}/configurations`, {params}).pipe( map(r => +r?.configurations?.[0]?.value || 1) ); } - /** Premier tax_rule_group actif (fallback TVA) */ - private getFirstActiveTaxRulesGroupId() { + /** Groupe de taxes par défaut (PS_TAX_DEFAULT_RULES_GROUP avec fallback sur le 1er groupe actif) */ + private getDefaultTaxRulesGroupId() { + const cfgParams = new HttpParams() + .set('display', '[value]') + .set('filter[name]', 'PS_TAX_DEFAULT_RULES_GROUP') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/configurations`, { params: cfgParams }).pipe( + switchMap(r => { + const raw = r?.configurations?.[0]?.value; + const cfgId = Number(raw); + + if (Number.isFinite(cfgId) && cfgId > 0) { + // Cas nominal : la conf est bien renseignée + return of(cfgId); + } + + // Fallback : on prend le 1er tax_rule_group actif + const trgParams = new HttpParams() + .set('display', '[id,name,active]') + .set('filter[active]', '1') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/tax_rule_groups`, { params: trgParams }).pipe( + map(tr => { + const rawGroups = tr?.tax_rule_groups ?? tr?.tax_rule_group ?? []; + const groups: any[] = Array.isArray(rawGroups) ? rawGroups : (rawGroups ? [rawGroups] : []); + const g = groups[0]; + const id = g ? Number(g.id) : 0; + if (!id) { + console.warn('[Presta] Aucun tax_rule_group actif trouvé, id_tax_rules_group sera 0 (pas de TVA).'); + } + return id; + }) + ); + }) + ); + } + + /** IDs des langues actives */ + getActiveLangIds() { const params = new HttpParams() .set('display', '[id,active]') .set('filter[active]', '1') .set('output_format', 'JSON'); - return this.http.get(`${this.base}/tax_rule_groups`, {params}).pipe( + return this.http.get(`${this.base}/languages`, {params}).pipe( + map(r => (r?.languages ?? []).map((l: any) => +l.id)) + ); + } + + /** Contexte boutique actif */ + private getDefaultShopContext() { + const params = new HttpParams() + .set('display', '[id,id_shop_group,active]') + .set('filter[active]', '1') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/shops`, {params}).pipe( map(r => { - const g = (r?.tax_rule_groups ?? [])[0]; - return g ? +g.id : 0; + const s = (r?.shops ?? [])[0]; + return { + idShop: s ? +s.id : 1, + idShopGroup: s ? +s.id_shop_group : 1 + }; }) ); } - /** Groupe de taxes par défaut (PS_TAX_DEFAULT_RULES_GROUP) avec fallback sur un groupe actif */ - private getDefaultTaxRulesGroupId() { + /** Id catégorie Home (PS_HOME_CATEGORY) */ + private getHomeCategoryId() { const params = new HttpParams() .set('display', '[value]') - .set('filter[name]', 'PS_TAX_DEFAULT_RULES_GROUP') + .set('filter[name]', 'PS_HOME_CATEGORY') .set('output_format', 'JSON'); return this.http.get(`${this.base}/configurations`, {params}).pipe( - switchMap(r => { - const id = +r?.configurations?.[0]?.value; - if (Number.isFinite(id) && id > 0) return of(id); - return this.getFirstActiveTaxRulesGroupId(); - }) + map(r => +r?.configurations?.[0]?.value || 2) ); } - /** Devise par défaut (PS_CURRENCY_DEFAULT) */ - private getDefaultCurrencyId() { + /** Id catégorie root (PS_ROOT_CATEGORY) */ + private getRootCategoryId() { const params = new HttpParams() .set('display', '[value]') - .set('filter[name]', 'PS_CURRENCY_DEFAULT') + .set('filter[name]', 'PS_ROOT_CATEGORY') .set('output_format', 'JSON'); return this.http.get(`${this.base}/configurations`, {params}).pipe( @@ -165,45 +216,38 @@ export class PrestashopService { ); } - /** IDs des langues actives (pour création catégorie multilangue) */ - getActiveLangIds() { - const params = new HttpParams().set('display', '[id,active]').set('filter[active]', '1').set('output_format', 'JSON'); - return this.http.get(`${this.base}/languages`, {params}).pipe( - map(r => (r?.languages ?? []).map((l: any) => +l.id)) - ); - } - - /** Contexte boutique actif (1ère boutique active) */ - private getDefaultShopContext() { - const params = new HttpParams() - .set('display', '[id,id_shop_group,active]') - .set('filter[active]', '1') - .set('output_format', 'JSON'); - return this.http.get(`${this.base}/shops`, {params}).pipe( - map(r => { - const s = (r?.shops ?? [])[0]; - return {idShop: s ? +s.id : 1, idShopGroup: s ? +s.id_shop_group : 1}; - }) - ); - } - - /** Objet complet pour update sûr (cat/manu/supplier) */ + /** Objet complet (JSON) pour update sûr */ private getOne(resource: Resource, id: number) { - const params = new HttpParams().set('output_format', 'JSON').set('display', 'full'); + const params = new HttpParams() + .set('output_format', 'JSON') + .set('display', 'full'); + return this.http.get(`${this.base}/${resource}/${id}`, {params}).pipe( map(r => r?.category ?? r?.manufacturer ?? r?.supplier ?? r) ); } // -------- CRUD générique (categories/manufacturers/suppliers) + list(resource: Resource) { - const params = new HttpParams().set('display', '[id,name,active]').set('output_format', 'JSON'); - return this.http.get(`${this.base}/${resource}`, {params}).pipe( - map(r => (r?.[resource] ?? []).map((x: any) => ({ - id: +x.id, - name: Array.isArray(x.name) ? (x.name[0]?.value ?? '') : (x.name ?? ''), - active: x.active === undefined ? undefined : !!+x.active - }) as PsItem)) + const params = new HttpParams() + .set('display', '[id,name,active]') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/${resource}`, { params }).pipe( + map(r => { + const arr = r?.[resource] ?? []; + const items: PsItem[] = arr.map((x: any) => ({ + id: +x.id, + name: Array.isArray(x.name) ? (x.name[0]?.value ?? '') : (x.name ?? ''), + active: x.active === undefined ? undefined : !!+x.active + }) as PsItem); + + // tri global A→Z par nom + return items.sort((a, b) => + a.name.localeCompare(b.name, 'fr', { sensitivity: 'base' }) + ); + }) ); } @@ -213,52 +257,72 @@ export class PrestashopService { if (resource === 'categories') { return this.getActiveLangIds().pipe( switchMap(langIds => { - const xml = ` + const xml = + ` 2 1 - ${this.toLangBlock('name', langIds.map((id: number) => ({id, value: safeName})))} - ${this.toLangBlock('link_rewrite', langIds.map((id: number) => ({id, value: this.slug(name)})))} + ${this.toLangBlock('name', langIds.map((id: any) => ({id, value: safeName})))} + ${this.toLangBlock('link_rewrite', langIds.map((id: any) => ({id, value: this.slug(name)})))} `; - return this.http.post(`${this.base}/categories`, xml, {headers: this.headersXml, responseType: 'text'}); + + return this.http.post(`${this.base}/categories`, xml, { + headers: this.headersXml, + responseType: 'text' + }); }), map(res => this.extractIdFromXml(res)) ); } - const xml = resource === 'manufacturers' - ? `1${safeName}` - : `1${safeName}`; + const xml = + resource === 'manufacturers' + ? `1${safeName}` + : `1${safeName}`; - return this.http.post(`${this.base}/${resource}`, xml, {headers: this.headersXml, responseType: 'text'}) - .pipe(map((res: string) => this.extractIdFromXml(res))); + return this.http.post(`${this.base}/${resource}`, xml, { + headers: this.headersXml, + responseType: 'text' + }).pipe( + map((res: string) => this.extractIdFromXml(res)) + ); } update(resource: Resource, id: number, newName: string) { const cfg = UPDATE_CFG[resource]; const safeName = this.escapeXml(newName); - const defaultLangOr1$ = (cfg.needsDefaultLang ? this.getDefaultLangId() : this.getDefaultLangId().pipe(map(() => 1))); + const defaultLangOr1$ = + cfg.needsDefaultLang ? this.getDefaultLangId() : this.getDefaultLangId().pipe(map(() => 1)); + return defaultLangOr1$.pipe( - switchMap((idLang: number) => this.getOne(resource, id).pipe( - switchMap(obj => { - const root = cfg.root; - const active = obj?.active === undefined ? 1 : +obj.active; + switchMap((idLang: number) => + this.getOne(resource, id).pipe( + switchMap(obj => { + const root = cfg.root; + const active = obj?.active === undefined ? 1 : +obj.active; - let linkRewriteXml = '', idParentXml = ''; - if (resource === 'categories') { - const lr = this.ensureArrayLang(obj?.link_rewrite); - linkRewriteXml = lr.length ? this.toLangBlock('link_rewrite', lr) - : this.toLangBlock('link_rewrite', [{id: idLang, value: this.slug(newName)}]); - if (obj?.id_parent) idParentXml = `${+obj.id_parent}`; - } + let linkRewriteXml = ''; + let idParentXml = ''; - const nameXml = cfg.nameIsMultilang - ? `${safeName}` - : `${safeName}`; + if (resource === 'categories') { + const lr = this.ensureArrayLang(obj?.link_rewrite); + linkRewriteXml = lr.length + ? this.toLangBlock('link_rewrite', lr) + : this.toLangBlock('link_rewrite', [{id: idLang, value: this.slug(newName)}]); - const body = ` + if (obj?.id_parent) { + idParentXml = `${+obj.id_parent}`; + } + } + + const nameXml = cfg.nameIsMultilang + ? `${safeName}` + : `${safeName}`; + + const body = + ` <${root}> ${id} ${active} @@ -268,53 +332,104 @@ export class PrestashopService { `; - return this.http.put(`${this.base}/${resource}/${id}`, body, { - headers: this.headersXml, - responseType: 'text' - }); - }) - )), + return this.http.put(`${this.base}/${resource}/${id}`, body, { + headers: this.headersXml, + responseType: 'text' + }); + }) + ) + ), map(() => true) ); } delete(resource: Resource, id: number) { - return this.http.delete(`${this.base}/${resource}/${id}`, {responseType: 'text'}).pipe(map(() => true)); + return this.http.delete(`${this.base}/${resource}/${id}`, { + responseType: 'text' + }).pipe(map(() => true)); } getXml(resource: Resource, id: number) { - return this.http.get(`${this.base}/${resource}/${id}`, {responseType: 'text'}); + return this.http.get(`${this.base}/${resource}/${id}`, { + responseType: 'text' + }); } // -------- Produits (liste / détails) + listProducts(query?: string) { let params = new HttpParams() .set('display', '[id,name,id_manufacturer,id_supplier,id_category_default,price]') .set('output_format', 'JSON'); - if (query?.trim()) params = params.set('filter[name]', `%[${query.trim()}]%`); + + if (query?.trim()) { + params = params.set('filter[name]', `%[${query.trim()}]%`); + } + + // 1) On récupère la liste des produits return this.http.get(`${this.base}/products`, {params}).pipe( - map(r => (r?.products ?? []).map((p: any) => ({ - id: +p.id, - name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''), - id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined, - id_supplier: p?.id_supplier ? +p.id_supplier : undefined, - id_category_default: p?.id_category_default ? +p.id_category_default : undefined, - priceHt: p?.price ? +p.price : 0 - }) as ProductListItem & { priceHt?: number })) + switchMap(r => { + const products: (ProductListItem & { priceHt?: number })[] = + (r?.products ?? []).map((p: any) => ({ + id: +p.id, + name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''), + id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined, + id_supplier: p?.id_supplier ? +p.id_supplier : undefined, + id_category_default: p?.id_category_default ? +p.id_category_default : undefined, + priceHt: p?.price ? +p.price : 0 + })); + + if (!products.length) { + return of([] as (ProductListItem & { priceHt?: number; quantity?: number })[]); + } + + const ids = products.map(p => p.id).join('|'); + + // 2) Une seule requête pour toutes les quantités + const stockParams = new HttpParams() + .set('display', '[id_product,quantity]') + .set('filter[id_product]', `[${ids}]`) + .set('filter[id_product_attribute]', '0') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/stock_availables`, {params: stockParams}).pipe( + map(sr => { + const saArr: any[] = Array.isArray(sr?.stock_availables) + ? sr.stock_availables + : (sr?.stock_availables ? [sr.stock_availables] : []); + + const qtyMap = new Map(); + for (const sa of saArr) { + const pid = +sa.id_product; + const q = sa.quantity != null ? +sa.quantity : 0; + qtyMap.set(pid, q); + } + + return products.map(p => ({ + ...p, + quantity: qtyMap.get(p.id) ?? 0 + })); + }) + ); + }) ); } - /** Détails produit (JSON full) + fallback XML pour description si vide */ + /** Détails produit (JSON full) + fallback XML si besoin */ getProductDetails(id: number) { - const params = new HttpParams().set('output_format', 'JSON').set('display', 'full'); + const params = new HttpParams() + .set('output_format', 'JSON') + .set('display', 'full'); + return this.http.get(`${this.base}/products/${id}`, {params}).pipe( - map(r => r?.product ?? r), + map(r => r?.products?.[0] ?? r?.product ?? r), switchMap(p => { - let description = Array.isArray(p?.description) ? (p.description[0]?.value ?? '') : (p?.description ?? ''); + let description = p?.description ?? ''; + if (description && typeof description === 'string') { return of({ id: +p.id, - name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''), + name: p.name, description, id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined, id_supplier: p?.id_supplier ? +p.id_supplier : undefined, @@ -322,17 +437,20 @@ export class PrestashopService { priceHt: p?.price ? +p.price : 0 }); } - // Fallback XML : extraire et enlever CDATA si présent + + // Fallback XML return this.http.get(`${this.base}/products/${id}`, {responseType: 'text'}).pipe( map((xml: string) => { - const m = xml.match(/[\s\S]*?]*>([\s\S]*?)<\/language>[\s\S]*?<\/description>/i); + const m = /[\s\S]*?]*>([\s\S]*?)<\/language>[\s\S]*?<\/description>/i.exec(xml); let descXml = m ? m[1] : ''; + if (descXml.startsWith('')) { descXml = descXml.slice(9, -3); } + return { id: +p.id, - name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''), + name: p.name, description: descXml, id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined, id_supplier: p?.id_supplier ? +p.id_supplier : undefined, @@ -345,9 +463,12 @@ export class PrestashopService { ); } - // --- Images + // -------- Images + private getProductImageIds(productId: number) { - return this.http.get(`${this.base}/images/products/${productId}`, {responseType: 'json' as any}).pipe( + return this.http.get(`${this.base}/images/products/${productId}`, { + responseType: 'json' as any + }).pipe( map(r => { const arr = (r?.image ?? r?.images ?? []) as Array; return Array.isArray(arr) ? arr.map(x => +x.id) : []; @@ -367,36 +488,36 @@ export class PrestashopService { return this.http.post(`${this.base}/images/products/${productId}`, fd); } - // --- Stock (quantité) — sans POST, uniquement PUT - /** Lis la quantité (prend exactement la ligne stock_available utilisée par Presta) */ + // -------- Stock (quantité) — gestion fine via stock_availables + getProductQuantity(productId: number) { - // 1) on essaie d’abord de récupérer l’id SA via les associations du produit (le plus fiable) + // 1) essayer de récupérer l’id SA via les associations du produit return this.http.get(`${this.base}/products/${productId}`, {responseType: 'text'}).pipe( switchMap(xml => { - const m = xml.match( - /]*>[\s\S]*?]*>\s*(?:)?\s*<\/id>[\s\S]*?<\/stock_availables>/i - ); + const m = /]*>[\s\S]*?]*>\s*(?:)?\s*<\/id>[\s\S]*?<\/stock_availables>/i.exec(xml); const saId = m ? +m[1] : null; + if (saId) { - // lire la ligne SA par son id → on lit quantity const params = new HttpParams() .set('display', '[id,quantity]') .set('filter[id]', `${saId}`) .set('output_format', 'JSON'); + return this.http.get(`${this.base}/stock_availables`, {params}).pipe( map(r => { const sa = (r?.stock_availables ?? [])[0]; - return sa?.quantity != null ? +sa.quantity : 0; + return sa?.quantity == null ? 0 : +sa.quantity; }) ); } - // 2) fallback: rechercher la ligne par id_product_attribute=0 (sans filtre shop → puis avec) + // 2) fallback : recherche par id_product_attribute=0 let p1 = new HttpParams() .set('display', '[id,quantity,id_product_attribute]') .set('filter[id_product]', `${productId}`) .set('filter[id_product_attribute]', '0') .set('output_format', 'JSON'); + return this.http.get(`${this.base}/stock_availables`, {params: p1}).pipe( switchMap(r => { const sa = (r?.stock_availables ?? [])[0]; @@ -415,7 +536,7 @@ export class PrestashopService { return this.http.get(`${this.base}/stock_availables`, {params: p2}).pipe( map(r2 => { const sa2 = (r2?.stock_availables ?? [])[0]; - return sa2?.quantity != null ? +sa2.quantity : 0; + return sa2?.quantity == null ? 0 : +sa2.quantity; }) ); }) @@ -426,19 +547,17 @@ export class PrestashopService { ); } - /** PUT quantité : on cible précisément la ligne SA (avec id_shop/id_shop_group réels) */ private setProductQuantity(productId: number, quantity: number) { const q = Math.max(0, Math.trunc(quantity)); - // Helper: fait un PUT en reprenant tous les champs utiles de la ligne SA existante const putFromRow = (row: any) => { const saId = +row.id; const idShop = row.id_shop ? +row.id_shop : undefined; const idShopGroup = row.id_shop_group ? +row.id_shop_group : undefined; const extraShop = - (idShop != null ? `${idShop}` : '') + - (idShopGroup != null ? `${idShopGroup}` : ''); + (idShop == null ? '' : `${idShop}`) + + (idShopGroup == null ? '' : `${idShopGroup}`); const xml = ` @@ -458,29 +577,27 @@ export class PrestashopService { }).pipe(map(() => true)); }; - // 1) récupérer l’id SA via les associations du produit (le plus fiable) return this.http.get(`${this.base}/products/${productId}`, {responseType: 'text'}).pipe( switchMap(xml => { - const m = xml.match( - /]*>[\s\S]*?]*>\s*(?:)?\s*<\/id>[\s\S]*?<\/stock_availables>/i - ); + const m = /]*>[\s\S]*?]*>\s*(?:)?\s*<\/id>[\s\S]*?<\/stock_availables>/i.exec(xml); const saId = m ? +m[1] : null; + if (saId) { const params = new HttpParams() .set('display', '[id,id_product,id_product_attribute,id_shop,id_shop_group,quantity]') .set('filter[id]', `${saId}`) .set('output_format', 'JSON'); + return this.http.get(`${this.base}/stock_availables`, {params}).pipe( switchMap(r => { const row = (r?.stock_availables ?? [])[0]; if (row?.id) return putFromRow(row); - // si l’id renvoyé n’est pas lisible (peu probable), on bascule sur le fallback ci-dessous - return of(null); + return of(true); }) ); } - // 2) fallback: rechercher la ligne par id_product_attribute=0 (sans filtre shop → puis avec) + // fallback let p1 = new HttpParams() .set('display', '[id,id_product,id_product_attribute,id_shop,id_shop_group,quantity]') .set('filter[id_product]', `${productId}`) @@ -508,7 +625,7 @@ export class PrestashopService { if (row2?.id) return putFromRow(row2); console.warn('[Presta] Aucune ligne stock_available PUTtable trouvée pour product:', productId); - return of(true); // on ne bloque pas le flux si la ligne est introuvable + return of(true); }) ); }) @@ -519,62 +636,268 @@ export class PrestashopService { ); } - // --- Association fournisseur (product_suppliers) - private findProductSupplierId(productId: number, supplierId: number) { - const params = new HttpParams() - .set('display', '[id,id_product,id_supplier,id_product_attribute]') - .set('filter[id_product]', `${productId}`) - .set('filter[id_supplier]', `${supplierId}`) - .set('filter[id_product_attribute]', '0') + // -------- Caractéristiques (Complet / Notice / Etat) + + /** Lit les caractéristiques Complet / Notice / État pour un produit donné. */ + getProductFlags(productId: number): Observable<{ complete: boolean; hasManual: boolean; conditionLabel?: string }> { + const prodParams = new HttpParams() + .set('output_format', 'JSON') + .set('display', 'full'); + + const featParams = new HttpParams() + .set('display', '[id,name]') .set('output_format', 'JSON'); - return this.http.get(`${this.base}/product_suppliers`, {params}).pipe( - map(r => { - const row = (r?.product_suppliers ?? [])[0]; - return row?.id ? +row.id : null; - }) - ); - } - - private upsertProductSupplier(productId: number, supplierId: number) { - if (!supplierId) return of(true); + const valParams = new HttpParams() + .set('display', '[id,id_feature,value]') + .set('output_format', 'JSON'); return forkJoin({ - curId: this.getDefaultCurrencyId(), - psId: this.findProductSupplierId(productId, supplierId), + prod: this.http.get(`${this.base}/products/${productId}`, { params: prodParams }), + features: this.http.get(`${this.base}/product_features`, { params: featParams }), + values: this.http.get(`${this.base}/product_feature_values`, { params: valParams }), }).pipe( - switchMap(({curId, psId}) => { - const body = (id?: number) => ` - - ${id ? `${id}` : ''} - ${productId} - 0 - ${supplierId} - ${curId} - - 0 - -`; + map(({ prod, features, values }) => { + // ----- Produit + const p = prod?.product ?? (prod?.products?.[0] ?? prod); + const rawPf = p?.associations?.product_features ?? []; + const pfArr: any[] = Array.isArray(rawPf) ? rawPf : (rawPf ? [rawPf] : []); - if (psId) { - return this.http.put(`${this.base}/product_suppliers/${psId}`, body(psId), { - headers: this.headersXml, responseType: 'text' - }).pipe(map(() => true)); - } else { - return this.http.post(`${this.base}/product_suppliers`, body(), { - headers: this.headersXml, responseType: 'text' - }).pipe(map(() => true)); + // ----- Caractéristiques (id -> nom) + const rawFeat = features?.product_features ?? features?.product_feature ?? []; + const featArr: any[] = Array.isArray(rawFeat) ? rawFeat : (rawFeat ? [rawFeat] : []); + + const featById = new Map(); + for (const f of featArr) { + const rawName = f.name; + const label = Array.isArray(rawName) ? (rawName[0]?.value ?? '') : (rawName ?? ''); + featById.set(+f.id, String(label)); } + + // ----- Valeurs (id -> { featureId, value }) + const rawVals = values?.product_feature_values ?? values?.product_feature_value ?? []; + const valArr: any[] = Array.isArray(rawVals) ? rawVals : (rawVals ? [rawVals] : []); + + const valById = new Map(); + for (const v of valArr) { + const rawVal = v.value; + const label = Array.isArray(rawVal) ? (rawVal[0]?.value ?? '') : (rawVal ?? ''); + valById.set(+v.id, { + featureId: +v.id_feature, + value: String(label), + }); + } + + // ----- Parcours des features du produit + let complete = false; + let hasManual = false; + let conditionLabel: string | undefined; + + for (const pf of pfArr) { + // format classique Presta: { id: id_feature, id_feature_value: id valeur } + const valueId = pf.id_feature_value == null ? (pf.id_value != null ? +pf.id_value : NaN) : +pf.id_feature_value; + if (!Number.isFinite(valueId)) continue; + + const vInfo = valById.get(valueId); + if (!vInfo) continue; + + const featName = featById.get(vInfo.featureId) ?? ''; + const featNorm = this.normalizeLabel(featName); + const valNorm = this.normalizeLabel(vInfo.value); + + if (featNorm === 'complet') { + // Complet: Oui / Non + if (valNorm === 'oui') complete = true; + if (valNorm === 'non') complete = false; + } else if (featNorm === 'notice') { + // Notice: Avec / Sans + if (valNorm === 'avec') hasManual = true; + if (valNorm === 'sans') hasManual = false; + } else if (featNorm === 'etat') { + // État: on garde juste le libellé brut + conditionLabel = vInfo.value; + } + } + + return { complete, hasManual, conditionLabel }; }) ); } - // --- Create & Update produit + /** Resolve un couple (featureId, valueId) à partir du nom de caractéristique et de la valeur */ + private resolveFeatureValue(featureName: string, valueLabel: string): Observable<{ + featureId: number; + valueId: number + } | null> { + if (!valueLabel) return of(null); + + // 1) récupérer toutes les caractéristiques, trouver celle dont le nom normalisé correspond + const paramsFeat = new HttpParams() + .set('display', '[id,name]') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/product_features`, {params: paramsFeat}).pipe( + switchMap(r => { + const rawFeat = r?.product_features ?? r?.product_feature ?? []; + const featArr: any[] = Array.isArray(rawFeat) ? rawFeat : (rawFeat ? [rawFeat] : []); + + const targetNorm = this.normalizeLabel(featureName); + const feat = featArr.find((f: any) => { + const raw = f.name; + const name = Array.isArray(raw) ? (raw[0]?.value ?? '') : (raw ?? ''); + return this.normalizeLabel(name) === targetNorm; + }); + + if (!feat?.id) { + console.warn('[Presta] Caractéristique introuvable:', featureName); + return of(null); + } + + const idFeature = +feat.id; + + // 2) récupérer les valeurs associées à cette caractéristique, trouver la bonne par libellé + const paramsVal = new HttpParams() + .set('display', '[id,id_feature,value]') + .set('filter[id_feature]', `${idFeature}`) + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/product_feature_values`, {params: paramsVal}).pipe( + map(rv => { + const rawVals = rv?.product_feature_values ?? rv?.product_feature_value ?? []; + const valArr: any[] = Array.isArray(rawVals) ? rawVals : (rawVals ? [rawVals] : []); + + const targetValNorm = this.normalizeLabel(valueLabel); + const val = valArr.find((v: any) => { + const raw = v.value; + const label = Array.isArray(raw) ? (raw[0]?.value ?? '') : (raw ?? ''); + return this.normalizeLabel(label) === targetValNorm; + }); + + if (!val?.id) { + console.warn('[Presta] Valeur de caractéristique introuvable:', featureName, valueLabel); + return null; + } + + return {featureId: idFeature, valueId: +val.id}; + }) + ); + }) + ); + } + + /** Construit le bloc pour Complet/Notice/Etat */ + private buildFeaturesXml(dto: PsProduct): Observable { + const ops: Array> = [ + this.resolveFeatureValue('Complet', dto.complete ? 'Oui' : 'Non'), + this.resolveFeatureValue('Notice', dto.hasManual ? 'Avec' : 'Sans') + ]; + + if (dto.conditionLabel) { + ops.push(this.resolveFeatureValue('Etat', dto.conditionLabel)); + } + + return forkJoin(ops).pipe( + map(results => { + const feats = results.filter((x): x is { featureId: number; valueId: number } => !!x); + if (!feats.length) return ''; + + const inner = feats.map(f => + `${f.featureId}${f.valueId}` + ).join(''); + + return `${inner}`; + }) + ); + } + +// -------- Helpers internes (produit complet pour update) + +// -------- Helpers internes (produit complet pour update) private getProductForUpdate(id: number) { const params = new HttpParams().set('output_format', 'JSON').set('display', 'full'); - return this.http.get(`${this.base}/products/${id}`, {params}).pipe(map(r => r?.product ?? r)); + + return this.http.get(`${this.base}/products/${id}`, {params}).pipe( + map(r => { + // format 1 : { product: { ... } } + if (r?.product) { + return r.product; + } + // format 2 : { products: [ { ... } ] } + if (Array.isArray(r?.products) && r.products.length) { + return r.products[0]; + } + // fallback : on renvoie brut + return r; + }) + ); } + /** Liste les valeurs possibles de la caractéristique "Etat" (libellés) */ + /** Liste les valeurs possibles de la caractéristique "État" (libellés) */ + /** Liste les valeurs possibles de la caractéristique "État" (avec cache mémoire) */ + getConditionValues(): Observable { + if (this.conditionValuesCache) { + return of(this.conditionValuesCache); + } + + const paramsFeat = new HttpParams() + .set('display', '[id,name]') + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/product_features`, {params: paramsFeat}).pipe( + switchMap(r => { + const rawFeat = r?.product_features ?? r?.product_feature ?? []; + const featArr: any[] = Array.isArray(rawFeat) ? rawFeat : (rawFeat ? [rawFeat] : []); + + const targetNorm = this.normalizeLabel('État'); // "État", "etat", etc. + const feat = featArr.find((f: any) => { + const raw = f.name; + const name = Array.isArray(raw) ? (raw[0]?.value ?? '') : (raw ?? ''); + return this.normalizeLabel(name) === targetNorm; + }); + + if (!feat?.id) { + console.warn('[Presta] Caractéristique "État" introuvable'); + this.conditionValuesCache = []; + return of([]); + } + + const idFeature = +feat.id; + + const paramsVal = new HttpParams() + .set('display', '[id,id_feature,value]') + .set('filter[id_feature]', `${idFeature}`) + .set('output_format', 'JSON'); + + return this.http.get(`${this.base}/product_feature_values`, {params: paramsVal}).pipe( + map(rv => { + const rawVals = rv?.product_feature_values ?? rv?.product_feature_value ?? []; + const vals: any[] = Array.isArray(rawVals) ? rawVals : (rawVals ? [rawVals] : []); + + const list = vals + .map(v => { + const raw = v.value; + if (Array.isArray(raw)) return raw[0]?.value ?? ''; + return String(raw ?? ''); + }) + .filter((x: string) => !!x); + + this.conditionValuesCache = list; + return list; + }) + ); + }) + ); + } + + // -------- Description (désormais: uniquement la description libre) + + private buildAugmentedDescription(dto: PsProduct): string { + return dto.description?.trim() || ''; + } + + // -------- Create & Update produit + createProduct(dto: PsProduct) { const priceHt = this.ttcToHt(dto.priceTtc, dto.vatRate); @@ -584,11 +907,13 @@ export class PrestashopService { shop: this.getDefaultShopContext(), homeCat: this.getHomeCategoryId(), rootCat: this.getRootCategoryId(), + featuresXml: this.buildFeaturesXml(dto), }).pipe( - switchMap(({idLang, idTaxGroup, shop, homeCat, rootCat}) => { + switchMap(({idLang, idTaxGroup, shop, homeCat, rootCat, featuresXml}) => { const desc = this.buildAugmentedDescription(dto); - const xml = ` + const xml = + ` ${dto.manufacturerId} ${dto.supplierId} @@ -614,22 +939,34 @@ export class PrestashopService { + ${rootCat} + ${homeCat} ${dto.categoryId} + ${featuresXml} `; - return this.http.post(`${this.base}/products`, xml, {headers: this.headersXml, responseType: 'text'}) - .pipe(map((res: string) => this.extractIdFromXml(res))); + return this.http.post(`${this.base}/products`, xml, { + headers: this.headersXml, + responseType: 'text' + }).pipe( + map((res: string) => this.extractIdFromXml(res)) + ); }), switchMap((productId) => { if (!productId) return of(null); + const ops: Array> = []; - if (dto.images?.length) ops.push(forkJoin(dto.images.map(f => this.uploadProductImage(productId, f)))); + + if (dto.images?.length) { + ops.push(forkJoin(dto.images.map(f => this.uploadProductImage(productId, f)))); + } + ops.push(this.setProductQuantity(productId, dto.quantity)); - if (dto.supplierId) ops.push(this.upsertProductSupplier(productId, dto.supplierId)); - return forkJoin(ops).pipe(map(() => productId)); + + return ops.length ? forkJoin(ops).pipe(map(() => productId)) : of(productId); }) ); } @@ -644,24 +981,26 @@ export class PrestashopService { shop: this.getDefaultShopContext(), homeCat: this.getHomeCategoryId(), rootCat: this.getRootCategoryId(), + featuresXml: this.buildFeaturesXml(dto), }).pipe( - switchMap(({idLang, prod, idTaxGroup, shop, homeCat, rootCat}) => { + switchMap(({idLang, prod, idTaxGroup, shop, homeCat, rootCat, featuresXml}) => { const active = +prod?.active || 1; const lr = Array.isArray(prod?.link_rewrite) ? prod.link_rewrite.map((l: any) => ({id: +l.id, value: String(l.value ?? '')})) : [{id: idLang, value: this.slug(dto.name)}]; - const lrXml = `${ - lr.map((e: { - id: any; - value: string; - }) => `${this.escapeXml(e.value)}`).join('') - }`; + const lrXml = + `${ + lr.map((e: { id: any; value: string }) => + `${this.escapeXml(e.value)}` + ).join('') + }`; const desc = this.buildAugmentedDescription(dto); - const xml = ` + const xml = + ` ${id} ${active} @@ -672,10 +1011,10 @@ export class PrestashopService { ${shop.idShop} ${idTaxGroup} - standard - standard - 1 - 1 + standard + standard + 1 + 1 ${priceHt} both @@ -689,38 +1028,31 @@ export class PrestashopService { + ${rootCat} + ${homeCat} ${dto.categoryId} + ${featuresXml} `; - return this.http.put(`${this.base}/products/${id}`, xml, {headers: this.headersXml, responseType: 'text'}) - .pipe(map(() => id)); - }), - switchMap(productId => { - const ops: Array> = [this.setProductQuantity(productId, dto.quantity)]; - if (dto.supplierId) ops.push(this.upsertProductSupplier(productId, dto.supplierId)); - return forkJoin(ops).pipe(map(() => true)); + return this.http.put(`${this.base}/products/${id}`, xml, { + headers: this.headersXml, + responseType: 'text' + }).pipe( + switchMap(() => this.setProductQuantity(id, dto.quantity)), + map(() => true) + ); }) ); } deleteProduct(id: number) { - return this.http.delete(`${this.base}/products/${id}`, {responseType: 'text'}).pipe(map(() => true)); - } - - // -------- Description augmentée - private buildAugmentedDescription(dto: PsProduct): string { - const parts: string[] = []; - if (dto.description?.trim()) parts.push(dto.description.trim()); - - const flags: string[] = []; - flags.push(`Complet: ${dto.complete ? 'Oui' : 'Non'}`); - flags.push(`Notice: ${dto.hasManual ? 'Oui' : 'Non'}`); - if (dto.conditionLabel) flags.push(`État: ${dto.conditionLabel}`); - - parts.push('', flags.join(' | ')); - return parts.join('\n').trim(); + return this.http.delete(`${this.base}/products/${id}`, { + responseType: 'text' + }).pipe( + map(() => true) + ); } }