feat: implement image carousel in product dialog; enhance image upload handling and preview functionality
This commit is contained in:
@@ -17,3 +17,17 @@
|
|||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prod-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prod-thumb {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {Component, inject, OnInit, ViewChild} from '@angular/core';
|
|||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef,
|
MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef,
|
||||||
MatHeaderRow, MatHeaderRowDef, MatNoDataRow, MatRow, MatRowDef,
|
MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
|
||||||
MatTable, MatTableDataSource
|
MatNoDataRow, MatTable, MatTableDataSource
|
||||||
} from '@angular/material/table';
|
} from '@angular/material/table';
|
||||||
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
|
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
|
||||||
import {MatSort, MatSortModule} from '@angular/material/sort';
|
import {MatSort, MatSortModule} from '@angular/material/sort';
|
||||||
@@ -13,6 +13,7 @@ import {MatButton, MatIconButton} from '@angular/material/button';
|
|||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
|
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
|
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
|
||||||
|
import {forkJoin} from 'rxjs';
|
||||||
|
|
||||||
import {PsItem} from '../../interfaces/ps-item';
|
import {PsItem} from '../../interfaces/ps-item';
|
||||||
import {ProductListItem} from '../../interfaces/product-list-item';
|
import {ProductListItem} from '../../interfaces/product-list-item';
|
||||||
@@ -60,19 +61,23 @@ export class PsProductCrudComponent implements OnInit {
|
|||||||
filterCtrl = this.fb.control<string>('');
|
filterCtrl = this.fb.control<string>('');
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// charger référentiels en parallèle
|
forkJoin({
|
||||||
Promise.all([
|
cats: this.ps.list('categories'),
|
||||||
this.ps.list('categories').toPromise(),
|
mans: this.ps.list('manufacturers'),
|
||||||
this.ps.list('manufacturers').toPromise(),
|
sups: this.ps.list('suppliers')
|
||||||
this.ps.list('suppliers').toPromise()
|
}).subscribe({
|
||||||
]).then(([cats, mans, sups]) => {
|
next: ({cats, mans, sups}) => {
|
||||||
this.categories = cats ?? [];
|
this.categories = cats ?? [];
|
||||||
this.catMap = new Map(this.categories.map(x => [x.id, x.name]));
|
this.catMap = new Map(this.categories.map(x => [x.id, x.name]));
|
||||||
this.manufacturers = mans ?? [];
|
this.manufacturers = mans ?? [];
|
||||||
this.manMap = new Map(this.manufacturers.map(x => [x.id, x.name]));
|
this.manMap = new Map(this.manufacturers.map(x => [x.id, x.name]));
|
||||||
this.suppliers = sups ?? [];
|
this.suppliers = sups ?? [];
|
||||||
this.supMap = new Map(this.suppliers.map(x => [x.id, x.name]));
|
this.supMap = new Map(this.suppliers.map(x => [x.id, x.name]));
|
||||||
this.reload();
|
this.reload();
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
console.error('Erreur lors du chargement des référentiels', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// filtre client
|
// filtre client
|
||||||
@@ -115,7 +120,7 @@ export class PsProductCrudComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bindProducts(p: (ProductListItem & { priceHt?: number })[]) {
|
private bindProducts(p: (ProductListItem & { priceHt?: number })[]) {
|
||||||
const vat = 0.20; // valeur fixe utilisée pour calcul TTC en liste
|
const vat = 0.2;
|
||||||
this.dataSource.data = p.map(x => ({
|
this.dataSource.data = p.map(x => ({
|
||||||
...x,
|
...x,
|
||||||
categoryName: x.id_category_default ? (this.catMap.get(x.id_category_default) ?? '') : '',
|
categoryName: x.id_category_default ? (this.catMap.get(x.id_category_default) ?? '') : '',
|
||||||
|
|||||||
@@ -23,14 +23,98 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbs {
|
/* ===== Nouveau : carrousel ===== */
|
||||||
display: flex;
|
|
||||||
|
.carousel {
|
||||||
|
grid-column: span 12;
|
||||||
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbs img {
|
.carousel-main {
|
||||||
|
position: relative;
|
||||||
|
min-height: 220px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-main img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 280px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-nav-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-nav-btn.left {
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-nav-btn.right {
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
color: #757575;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-placeholder mat-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bandeau de vignettes */
|
||||||
|
|
||||||
|
.carousel-thumbs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-item {
|
||||||
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .2);
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-item.active {
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #eeeeee;
|
||||||
|
color: #575656;
|
||||||
|
border: 1px dashed #bdbdbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-placeholder mat-icon {
|
||||||
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,57 @@
|
|||||||
|
|
||||||
<div mat-dialog-content class="grid" [formGroup]="form">
|
<div mat-dialog-content class="grid" [formGroup]="form">
|
||||||
|
|
||||||
<!-- Input pour l'upload des images -->
|
<!-- CARROUSEL IMAGES -->
|
||||||
<div class="col-12">
|
<div class="col-12 carousel">
|
||||||
<label for="fileInput">Images du produit</label>
|
<div class="carousel-main">
|
||||||
<input type="file" multiple (change)="onFiles($event)">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Affichage des vignettes des images existantes en mode édition -->
|
<!-- Bouton précédent -->
|
||||||
@if (mode==='edit' && existingImageUrls.length) {
|
<button mat-icon-button
|
||||||
<div class="col-12">
|
class="carousel-nav-btn left"
|
||||||
<div class="thumbs">
|
(click)="prev()"
|
||||||
@for (url of existingImageUrls; track url) {
|
[disabled]="carouselItems.length <= 1">
|
||||||
<img [src]="url" alt="Produit">
|
<mat-icon>chevron_left</mat-icon>
|
||||||
}
|
</button>
|
||||||
</div>
|
|
||||||
|
<!-- Image principale ou placeholder -->
|
||||||
|
@if (carouselItems.length && !carouselItems[currentIndex].isPlaceholder) {
|
||||||
|
<img [src]="carouselItems[currentIndex].src" alt="Produit">
|
||||||
|
} @else {
|
||||||
|
<div class="carousel-placeholder" (click)="fileInput.click()">
|
||||||
|
<mat-icon>add_photo_alternate</mat-icon>
|
||||||
|
<span>Ajouter des images</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Bouton suivant -->
|
||||||
|
<button mat-icon-button
|
||||||
|
class="carousel-nav-btn right"
|
||||||
|
(click)="next()"
|
||||||
|
[disabled]="carouselItems.length <= 1">
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
<!-- Bandeau de vignettes -->
|
||||||
|
<div class="carousel-thumbs">
|
||||||
|
@for (item of carouselItems; let i = $index; track item) {
|
||||||
|
<div class="thumb-item"
|
||||||
|
[class.active]="i === currentIndex"
|
||||||
|
(click)="onThumbClick(i)">
|
||||||
|
@if (!item.isPlaceholder) {
|
||||||
|
<img class="thumb-img" [src]="item.src" alt="Vignette produit">
|
||||||
|
} @else {
|
||||||
|
<div class="thumb-placeholder" (click)="fileInput.click()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input réel, caché -->
|
||||||
|
<input #fileInput type="file" multiple hidden (change)="onFiles($event)">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Input pour le nom du produit -->
|
<!-- Input pour le nom du produit -->
|
||||||
<mat-form-field class="col-12">
|
<mat-form-field class="col-12">
|
||||||
@@ -93,6 +128,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
<div mat-dialog-actions>
|
<div mat-dialog-actions>
|
||||||
<button mat-button (click)="close()">Annuler</button>
|
<button mat-button (click)="close()">Annuler</button>
|
||||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.invalid">
|
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.invalid">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Component, Inject, OnInit, inject } from '@angular/core';
|
import {Component, Inject, OnInit, inject, OnDestroy} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
||||||
import { MatInput } from '@angular/material/input';
|
import { MatInput } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
import { MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { MatButton } from '@angular/material/button';
|
import { MatButton, MatIconButton } from '@angular/material/button';
|
||||||
import {
|
import {
|
||||||
MatDialogRef,
|
MatDialogRef,
|
||||||
MAT_DIALOG_DATA,
|
MAT_DIALOG_DATA,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
MatDialogTitle
|
MatDialogTitle
|
||||||
} from '@angular/material/dialog';
|
} from '@angular/material/dialog';
|
||||||
|
import { MatIcon } from '@angular/material/icon';
|
||||||
|
|
||||||
import { catchError, forkJoin, of, Observable } from 'rxjs';
|
import { catchError, forkJoin, of, Observable } from 'rxjs';
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ export type ProductDialogData = {
|
|||||||
productRow?: ProductListItem & { priceHt?: number };
|
productRow?: ProductListItem & { priceHt?: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CarouselItem = { src: string; isPlaceholder: boolean };
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-ps-product-dialog',
|
selector: 'app-ps-product-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -34,10 +37,11 @@ export type ProductDialogData = {
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule, ReactiveFormsModule,
|
CommonModule, ReactiveFormsModule,
|
||||||
MatFormField, MatLabel, MatInput, MatSelectModule, MatCheckbox,
|
MatFormField, MatLabel, MatInput, MatSelectModule, MatCheckbox,
|
||||||
MatButton, MatDialogActions, MatDialogContent, MatDialogTitle
|
MatButton, MatDialogActions, MatDialogContent, MatDialogTitle,
|
||||||
|
MatIcon, MatIconButton
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PsProductDialogComponent implements OnInit {
|
export class PsProductDialogComponent implements OnInit, OnDestroy {
|
||||||
private readonly fb = inject(FormBuilder);
|
private readonly fb = inject(FormBuilder);
|
||||||
private readonly ps = inject(PrestashopService);
|
private readonly ps = inject(PrestashopService);
|
||||||
|
|
||||||
@@ -55,6 +59,13 @@ export class PsProductDialogComponent implements OnInit {
|
|||||||
images: File[] = [];
|
images: File[] = [];
|
||||||
existingImageUrls: string[] = [];
|
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.)
|
// options possibles pour l'état (Neuf, Très bon état, etc.)
|
||||||
conditionOptions: string[] = [];
|
conditionOptions: string[] = [];
|
||||||
|
|
||||||
@@ -178,15 +189,80 @@ export class PsProductDialogComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.existingImageUrls = imgs;
|
this.existingImageUrls = imgs;
|
||||||
|
this.buildCarousel();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// mode création : uniquement placeholder au début
|
||||||
|
this.buildCarousel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------- Carrousel / gestion des fichiers --------
|
||||||
|
|
||||||
onFiles(ev: Event) {
|
onFiles(ev: Event) {
|
||||||
const fl = (ev.target as HTMLInputElement).files;
|
const fl = (ev.target as HTMLInputElement).files;
|
||||||
this.images = fl ? Array.from(fl) : [];
|
|
||||||
|
// 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() {
|
save() {
|
||||||
if (this.form.invalid) return;
|
if (this.form.invalid) return;
|
||||||
|
|
||||||
@@ -199,7 +275,7 @@ export class PsProductDialogComponent implements OnInit {
|
|||||||
categoryId: +v.categoryId!,
|
categoryId: +v.categoryId!,
|
||||||
manufacturerId: +v.manufacturerId!,
|
manufacturerId: +v.manufacturerId!,
|
||||||
supplierId: +v.supplierId!,
|
supplierId: +v.supplierId!,
|
||||||
images: this.images,
|
images: this.images, // toujours les fichiers sélectionnés
|
||||||
complete: !!v.complete,
|
complete: !!v.complete,
|
||||||
hasManual: !!v.hasManual,
|
hasManual: !!v.hasManual,
|
||||||
conditionLabel: v.conditionLabel || undefined,
|
conditionLabel: v.conditionLabel || undefined,
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export interface PsProduct {
|
|||||||
priceTtc: number; // saisi côté UI (TTC)
|
priceTtc: number; // saisi côté UI (TTC)
|
||||||
vatRate?: number; // ex: 0.20 (20%). Défaut: 0.20 si non fourni
|
vatRate?: number; // ex: 0.20 (20%). Défaut: 0.20 si non fourni
|
||||||
quantity: number; // stock souhaité (pour id_product_attribute = 0)
|
quantity: number; // stock souhaité (pour id_product_attribute = 0)
|
||||||
|
thumbUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const UPDATE_CFG: Record<Resource, {
|
|||||||
export class PrestashopService {
|
export class PrestashopService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly base = '/ps';
|
private readonly base = '/ps';
|
||||||
|
private readonly frontBase = 'https://shop.gameovergne.fr'
|
||||||
|
|
||||||
// -------- Utils
|
// -------- Utils
|
||||||
private readonly headersXml = new HttpHeaders({
|
private readonly headersXml = new HttpHeaders({
|
||||||
@@ -465,20 +466,34 @@ export class PrestashopService {
|
|||||||
|
|
||||||
// -------- Images
|
// -------- Images
|
||||||
|
|
||||||
private getProductImageIds(productId: number) {
|
/** Retourne les URLs publiques des images du produit (FO Presta, pas l’API) */
|
||||||
return this.http.get<any>(`${this.base}/images/products/${productId}`, {
|
getProductImageUrls(productId: number) {
|
||||||
responseType: 'json' as any
|
const params = new HttpParams()
|
||||||
}).pipe(
|
.set('output_format', 'JSON')
|
||||||
|
.set('display', 'full');
|
||||||
|
|
||||||
|
return this.http.get<any>(`${this.base}/products/${productId}`, { params }).pipe(
|
||||||
map(r => {
|
map(r => {
|
||||||
const arr = (r?.image ?? r?.images ?? []) as Array<any>;
|
// même logique que pour les autres méthodes : format 1 ou 2
|
||||||
return Array.isArray(arr) ? arr.map(x => +x.id) : [];
|
const p = r?.product ?? (Array.isArray(r?.products) ? r.products[0] : r);
|
||||||
|
const rawAssoc = p?.associations ?? {};
|
||||||
|
const rawImages = rawAssoc?.images?.image ?? rawAssoc?.images ?? [];
|
||||||
|
const arr: any[] = Array.isArray(rawImages) ? rawImages : (rawImages ? [rawImages] : []);
|
||||||
|
|
||||||
|
const ids = arr
|
||||||
|
.map(img => +img.id)
|
||||||
|
.filter(id => Number.isFinite(id) && id > 0);
|
||||||
|
|
||||||
|
return ids.map(id => this.buildFrontImageUrl(id));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProductImageUrls(productId: number) {
|
/** Retourne la première URL d'image (miniature) du produit, ou null s'il n'y en a pas */
|
||||||
return this.getProductImageIds(productId).pipe(
|
getProductThumbnailUrl(productId: number) {
|
||||||
map(ids => ids.map(idImg => `${this.base}/images/products/${productId}/${idImg}`))
|
return this.getProductImageUrls(productId).pipe(
|
||||||
|
map(urls => urls.length ? urls[0] : null),
|
||||||
|
catchError(() => of(null))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,8 +825,6 @@ export class PrestashopService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Helpers internes (produit complet pour update)
|
|
||||||
|
|
||||||
// -------- Helpers internes (produit complet pour update)
|
// -------- Helpers internes (produit complet pour update)
|
||||||
private getProductForUpdate(id: number) {
|
private getProductForUpdate(id: number) {
|
||||||
const params = new HttpParams().set('output_format', 'JSON').set('display', 'full');
|
const params = new HttpParams().set('output_format', 'JSON').set('display', 'full');
|
||||||
@@ -890,8 +903,14 @@ export class PrestashopService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Description (désormais: uniquement la description libre)
|
/** Construit l’URL publique d’une image produit Presta à partir de son id_image */
|
||||||
|
private buildFrontImageUrl(imageId: number): string {
|
||||||
|
const idStr = String(imageId);
|
||||||
|
const path = idStr.split('').join('/'); // "123" -> "1/2/3"
|
||||||
|
return `${this.frontBase}/img/p/${path}/${idStr}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Description (désormais: uniquement la description libre)
|
||||||
private buildAugmentedDescription(dto: PsProduct): string {
|
private buildAugmentedDescription(dto: PsProduct): string {
|
||||||
return dto.description?.trim() || '';
|
return dto.description?.trim() || '';
|
||||||
}
|
}
|
||||||
@@ -1040,11 +1059,20 @@ export class PrestashopService {
|
|||||||
return this.http.put(`${this.base}/products/${id}`, xml, {
|
return this.http.put(`${this.base}/products/${id}`, xml, {
|
||||||
headers: this.headersXml,
|
headers: this.headersXml,
|
||||||
responseType: 'text'
|
responseType: 'text'
|
||||||
}).pipe(
|
});
|
||||||
switchMap(() => this.setProductQuantity(id, dto.quantity)),
|
}),
|
||||||
map(() => true)
|
switchMap(() => {
|
||||||
);
|
const ops: Array<Observable<unknown>> = [];
|
||||||
})
|
|
||||||
|
if (dto.images?.length) {
|
||||||
|
ops.push(forkJoin(dto.images.map(f => this.uploadProductImage(id, f))));
|
||||||
|
}
|
||||||
|
|
||||||
|
ops.push(this.setProductQuantity(id, dto.quantity));
|
||||||
|
|
||||||
|
return ops.length ? forkJoin(ops) : of(true);
|
||||||
|
}),
|
||||||
|
map(() => true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user