Merge branch 'back-office-integration'

# Conflicts:
#	src/app/pages/cart/cart.component.ts
#	src/app/pages/category/category.component.html
#	src/app/pages/category/category.component.ts
#	src/app/pages/item-detail/item-detail.component.html
#	src/app/pages/item-detail/item-detail.component.ts
#	src/app/pages/legal/company-details/en/company-details-en.component.html
#	src/app/pages/legal/company-details/hy/company-details-hy.component.html
#	src/app/pages/legal/company-details/ru/company-details-ru.component.html
#	src/app/pages/legal/public-offer/en/public-offer-en.component.html
#	src/app/pages/legal/public-offer/ru/public-offer-ru.component.html
#	src/app/pages/search/search.component.ts
#	src/app/services/api.service.ts
This commit is contained in:
sdarbinyan
2026-03-24 00:18:13 +04:00
76 changed files with 6828 additions and 2331 deletions

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, timer } from 'rxjs';
import { map, retry } from 'rxjs/operators';
import { Category, Item } from '../models';
import { Category, Item, Subcategory } from '../models';
import { environment } from '../../environments/environment';
@Injectable({
@@ -18,38 +18,174 @@ export class ApiService {
constructor(private http: HttpClient) {}
private normalizeItem(item: Item): Item {
return {
...item,
remainings: item.remainings || 'high'
};
/**
* Normalize an item from the API response — supports both
* legacy marketplace format and the new backOffice API format.
*/
private normalizeItem(raw: any): Item {
const { partnerID, ...rest } = raw;
const item: Item = { ...rest };
// Map backOffice string id → legacy numeric itemID
if (raw.id != null && raw.itemID == null) {
item.id = String(raw.id);
item.itemID = typeof raw.id === 'number' ? raw.id : 0;
}
// Map backOffice imgs[] → legacy photos[]
if (raw.imgs && (!raw.photos || raw.photos.length === 0)) {
item.photos = raw.imgs.map((url: string) => ({ url }));
}
// Normalize photo type: API sends type='video'|'photo', template checks .video
if (item.photos) {
item.photos = item.photos.map((p: any) => ({
...p,
video: p.video || (p.type === 'video' ? p.url : undefined),
}));
}
item.imgs = raw.imgs || raw.photos?.map((p: any) => p.url) || [];
// Map backOffice description (key-value array) → legacy description string
if (Array.isArray(raw.description)) {
item.descriptionFields = raw.description;
item.description = raw.description.map((d: any) => `${d.key}: ${d.value}`).join('\n');
} else {
item.description = raw.description || raw.simpleDescription || '';
}
// Map backend names[] → translations (multi-lang name support)
if (raw.names && Array.isArray(raw.names)) {
item.names = raw.names;
if (!item.translations) item.translations = {};
for (const entry of raw.names) {
if (!item.translations[entry.language]) item.translations[entry.language] = {};
item.translations[entry.language].name = entry.value;
}
}
// Map backend descriptions[] → translations (multi-lang descriptions)
if (raw.descriptions && Array.isArray(raw.descriptions)) {
item.descriptions = raw.descriptions;
if (!item.translations) item.translations = {};
for (const entry of raw.descriptions) {
if (!item.translations[entry.language]) item.translations[entry.language] = {};
item.translations[entry.language].simpleDescription = entry.value;
}
}
// Preserve attributes from backend
item.attributes = raw.attributes || [];
// Preserve colour & size
item.colour = raw.colour || '';
item.size = raw.size || '';
// Map backOffice comments → legacy callbacks
if (raw.comments && (!raw.callbacks || raw.callbacks.length === 0)) {
item.callbacks = raw.comments.map((c: any) => ({
rating: c.stars,
content: c.text,
userID: c.author,
timestamp: c.createdAt,
}));
}
item.comments = raw.comments || raw.callbacks?.map((c: any) => ({
id: c.userID,
text: c.content,
author: c.userID,
stars: c.rating,
createdAt: c.timestamp,
})) || [];
// Compute average rating from comments if not present
if (raw.rating == null && item.comments && item.comments.length > 0) {
const rated = item.comments.filter(c => c.stars != null);
item.rating = rated.length > 0
? rated.reduce((sum, c) => sum + (c.stars || 0), 0) / rated.length
: 0;
}
item.rating = item.rating || 0;
// Defaults
item.discount = item.discount || 0;
item.remainings = item.remainings || (raw.quantity != null
? (raw.quantity <= 0 ? 'out' : raw.quantity <= 5 ? 'low' : raw.quantity <= 20 ? 'medium' : 'high')
: 'high');
item.currency = item.currency || 'RUB';
// Preserve new backOffice fields
item.badges = raw.badges || [];
item.tags = raw.tags || [];
item.simpleDescription = raw.simpleDescription || '';
item.translations = item.translations || raw.translations || {};
item.visible = raw.visible ?? true;
item.priority = raw.priority ?? 0;
return item;
}
private normalizeItems(items: Item[] | null | undefined): Item[] {
private normalizeItems(items: any[] | null | undefined): Item[] {
if (!items || !Array.isArray(items)) {
return [];
}
return items.map(item => this.normalizeItem(item));
}
/**
* Normalize a category from the API response — supports both
* the flat legacy format and nested backOffice format.
*/
private normalizeCategory(raw: any): Category {
const cat: Category = { ...raw };
if (raw.id != null && raw.categoryID == null) {
cat.id = String(raw.id);
cat.categoryID = typeof raw.id === 'number' ? raw.id : 0;
}
// Map backOffice img → legacy icon
if (raw.img && !raw.icon) {
cat.icon = raw.img;
}
cat.img = raw.img || raw.icon;
cat.parentID = raw.parentID ?? 0;
cat.visible = raw.visible ?? true;
cat.priority = raw.priority ?? 0;
if (raw.subcategories && Array.isArray(raw.subcategories)) {
cat.subcategories = raw.subcategories;
}
return cat;
}
private normalizeCategories(cats: any[] | null | undefined): Category[] {
if (!cats || !Array.isArray(cats)) return [];
return cats.map(c => this.normalizeCategory(c));
}
// ─── Core Marketplace Endpoints ───────────────────────────
ping(): Observable<{ message: string }> {
return this.http.get<{ message: string }>(`${this.baseUrl}/ping`);
}
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(`${this.baseUrl}/category`).pipe(retry(this.retryConfig));
return this.http.get<any[]>(`${this.baseUrl}/category`)
.pipe(retry(this.retryConfig), map(cats => this.normalizeCategories(cats)));
}
getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable<Item[]> {
const params = new HttpParams()
.set('count', count.toString())
.set('skip', skip.toString());
return this.http.get<Item[]>(`${this.baseUrl}/category/${categoryID}`, { params })
return this.http.get<any[]>(`${this.baseUrl}/category/${categoryID}`, { params })
.pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
}
getItem(itemID: number): Observable<Item> {
return this.http.get<Item>(`${this.baseUrl}/item/${itemID}`)
return this.http.get<any>(`${this.baseUrl}/item/${itemID}`)
.pipe(retry(this.retryConfig), map(item => this.normalizeItem(item)));
}
@@ -58,7 +194,7 @@ export class ApiService {
.set('search', search)
.set('count', count.toString())
.set('skip', skip.toString());
return this.http.get<{ items: Item[], total: number, count: number, skip: number }>(`${this.baseUrl}/searchitems`, { params })
return this.http.get<any>(`${this.baseUrl}/searchitems`, { params })
.pipe(
retry(this.retryConfig),
map(response => ({
@@ -81,7 +217,7 @@ export class ApiService {
}
getCart(): Observable<Item[]> {
return this.http.get<Item[]>(`${this.baseUrl}/cart`)
return this.http.get<any[]>(`${this.baseUrl}/cart`)
.pipe(map(items => this.normalizeItems(items)));
}
@@ -167,7 +303,7 @@ export class ApiService {
if (categoryID) {
params = params.set('category', categoryID.toString());
}
return this.http.get<Item[]>(`${this.baseUrl}/randomitems`, { params })
return this.http.get<any[]>(`${this.baseUrl}/randomitems`, { params })
.pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
}
}