changes
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { ItemName } from './item.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-language translation content for a category or subcategory.
|
* Per-language translation content for a category or subcategory.
|
||||||
* Stored under `translations['ru']`, `translations['en']`, etc.
|
* Stored under `translations['ru']`, `translations['en']`, etc.
|
||||||
@@ -16,6 +18,15 @@ export interface Category {
|
|||||||
subcategories?: Subcategory[];
|
subcategories?: Subcategory[];
|
||||||
/** Optional translations keyed by language code: { ru: { name: '...' } } */
|
/** Optional translations keyed by language code: { ru: { name: '...' } } */
|
||||||
translations?: { [lang: string]: CategoryTranslation };
|
translations?: { [lang: string]: CategoryTranslation };
|
||||||
|
|
||||||
|
// Fields from Go backend struct
|
||||||
|
categoryID?: number;
|
||||||
|
parentID?: number;
|
||||||
|
icon?: string;
|
||||||
|
wideBanner?: string;
|
||||||
|
itemCount?: number;
|
||||||
|
categoriesCount?: number;
|
||||||
|
names?: ItemName[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Subcategory {
|
export interface Subcategory {
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ export interface ItemTranslation {
|
|||||||
export interface ItemName {
|
export interface ItemName {
|
||||||
language: string;
|
language: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
/** Backend typo — some responses use 'valuue' instead of 'value' */
|
||||||
|
valuue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Localized description entry */
|
/** Localized description entry */
|
||||||
export interface ItemDescription {
|
export interface ItemDescription {
|
||||||
language: string;
|
language: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
valuue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Key-value attribute pair */
|
/** Key-value attribute pair */
|
||||||
@@ -25,6 +28,39 @@ export interface ItemAttribute {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Item variant detail (price, size, colour per variant) */
|
||||||
|
export interface ItemDetail {
|
||||||
|
color?: string;
|
||||||
|
colour?: string;
|
||||||
|
size?: string;
|
||||||
|
price: number;
|
||||||
|
currency: string;
|
||||||
|
remaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Photo entry with type (photo or video) */
|
||||||
|
export interface Photo {
|
||||||
|
type?: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Question on an item */
|
||||||
|
export interface Question {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
like?: number;
|
||||||
|
dislike?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Review / callback on an item */
|
||||||
|
export interface Review {
|
||||||
|
rating?: number;
|
||||||
|
content?: string;
|
||||||
|
userID?: string;
|
||||||
|
answer?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Item {
|
export interface Item {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,6 +84,18 @@ export interface Item {
|
|||||||
comments?: Comment[];
|
comments?: Comment[];
|
||||||
/** Optional translations keyed by language code: { ru: { name: '...', simpleDescription: '...', description: [...] } } */
|
/** Optional translations keyed by language code: { ru: { name: '...', simpleDescription: '...', description: [...] } } */
|
||||||
translations?: { [lang: string]: ItemTranslation };
|
translations?: { [lang: string]: ItemTranslation };
|
||||||
|
|
||||||
|
// Fields from Go backend struct
|
||||||
|
itemID?: number;
|
||||||
|
categoryID?: number;
|
||||||
|
rating?: number;
|
||||||
|
visits?: number;
|
||||||
|
itemDetails?: ItemDetail[];
|
||||||
|
photos?: Photo[];
|
||||||
|
questions?: Question[];
|
||||||
|
callbacks?: Review[];
|
||||||
|
partnerID?: string;
|
||||||
|
remaining?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemDescriptionField {
|
export interface ItemDescriptionField {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class ItemEditorComponent implements OnInit {
|
|||||||
ruSimpleDesc = '';
|
ruSimpleDesc = '';
|
||||||
ruDescFields: ItemDescriptionField[] = [];
|
ruDescFields: ItemDescriptionField[] = [];
|
||||||
|
|
||||||
currencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH'];
|
currencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH', 'AMD'];
|
||||||
|
|
||||||
predefinedBadges: { label: string; value: string; color: string }[] = [
|
predefinedBadges: { label: string; value: string; color: string }[] = [
|
||||||
{ label: 'New', value: 'new', color: '#009688' },
|
{ label: 'New', value: 'new', color: '#009688' },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
|||||||
import { Observable, Subject, throwError } from 'rxjs';
|
import { Observable, Subject, throwError } from 'rxjs';
|
||||||
import { debounceTime, retry, catchError, map, groupBy, mergeMap } from 'rxjs/operators';
|
import { debounceTime, retry, catchError, map, groupBy, mergeMap } from 'rxjs/operators';
|
||||||
import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models';
|
import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models';
|
||||||
|
import { ItemName } from '../models/item.model';
|
||||||
import { MockDataService } from './mock-data.service';
|
import { MockDataService } from './mock-data.service';
|
||||||
import { ToastService } from './toast.service';
|
import { ToastService } from './toast.service';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
@@ -34,6 +35,108 @@ export class ApiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Normalizers ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get text from an ItemName entry, handling the backend 'valuue' typo */
|
||||||
|
private nameValue(entry: ItemName): string {
|
||||||
|
return entry.value || entry.valuue || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalize an item from the backend — extracts itemDetails, names, photos */
|
||||||
|
private normalizeItem(raw: any): Item {
|
||||||
|
const item: Item = { ...raw };
|
||||||
|
|
||||||
|
// Extract price/currency/remaining from itemDetails[0] if present
|
||||||
|
const details = raw.itemDetails || raw.itemdetails;
|
||||||
|
if (details && Array.isArray(details) && details.length > 0) {
|
||||||
|
const d = details[0];
|
||||||
|
item.itemDetails = details;
|
||||||
|
if (item.price == null || item.price === 0) item.price = d.price ?? 0;
|
||||||
|
if (!item.currency) item.currency = d.currency || 'RUB';
|
||||||
|
if (!item.colour) item.colour = d.colour || d.color || '';
|
||||||
|
if (!item.size) item.size = d.size || '';
|
||||||
|
if (item.quantity == null && d.remaining != null) item.quantity = d.remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build translations from names[]/descriptions[] if translations map is empty
|
||||||
|
if (raw.names && Array.isArray(raw.names)) {
|
||||||
|
item.translations = item.translations || {};
|
||||||
|
for (const n of raw.names) {
|
||||||
|
const lang = n.language?.toLowerCase();
|
||||||
|
const val = this.nameValue(n);
|
||||||
|
if (lang && val) {
|
||||||
|
item.translations[lang] = item.translations[lang] || {};
|
||||||
|
item.translations[lang].name = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (raw.descriptions && Array.isArray(raw.descriptions)) {
|
||||||
|
item.translations = item.translations || {};
|
||||||
|
for (const d of raw.descriptions) {
|
||||||
|
const lang = d.language?.toLowerCase();
|
||||||
|
const val = d.value || d.valuue || '';
|
||||||
|
if (lang && val) {
|
||||||
|
item.translations[lang] = item.translations[lang] || {};
|
||||||
|
item.translations[lang].simpleDescription = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build imgs[] from photos[] if imgs is empty
|
||||||
|
if ((!item.imgs || item.imgs.length === 0) && raw.photos && Array.isArray(raw.photos)) {
|
||||||
|
item.imgs = raw.photos.map((p: any) => p.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map callbacks → comments if comments is missing
|
||||||
|
if ((!item.comments || item.comments.length === 0) && raw.callbacks && Array.isArray(raw.callbacks)) {
|
||||||
|
item.comments = raw.callbacks.map((c: any) => ({
|
||||||
|
id: c.userID || '',
|
||||||
|
text: c.content || '',
|
||||||
|
author: c.userID || '',
|
||||||
|
stars: c.rating,
|
||||||
|
createdAt: c.timestamp,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
item.name = item.name || '';
|
||||||
|
item.price = item.price ?? 0;
|
||||||
|
item.discount = item.discount ?? 0;
|
||||||
|
item.quantity = item.quantity ?? 0;
|
||||||
|
item.currency = item.currency || 'RUB';
|
||||||
|
item.imgs = item.imgs || [];
|
||||||
|
item.tags = item.tags || [];
|
||||||
|
item.description = item.description || [];
|
||||||
|
item.simpleDescription = item.simpleDescription || '';
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalize a category — merges names[] into translations, maps Go struct fields */
|
||||||
|
private normalizeCategory(raw: any): Category {
|
||||||
|
const cat: Category = { ...raw };
|
||||||
|
|
||||||
|
if (raw.names && Array.isArray(raw.names)) {
|
||||||
|
cat.translations = cat.translations || {};
|
||||||
|
for (const n of raw.names) {
|
||||||
|
const lang = n.language?.toLowerCase();
|
||||||
|
const val = this.nameValue(n);
|
||||||
|
if (lang && val) {
|
||||||
|
cat.translations[lang] = cat.translations[lang] || {};
|
||||||
|
cat.translations[lang].name = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Go struct fields
|
||||||
|
if (raw.icon && !cat.img) cat.img = raw.icon;
|
||||||
|
if (raw.wideicon) cat.wideBanner = raw.wideicon;
|
||||||
|
if (raw.ItemsCount != null) cat.itemCount = raw.ItemsCount;
|
||||||
|
if (raw.CategoriesCount != null) cat.categoriesCount = raw.CategoriesCount;
|
||||||
|
|
||||||
|
return cat;
|
||||||
|
}
|
||||||
|
|
||||||
// Projects
|
// Projects
|
||||||
getProjects(): Observable<Project[]> {
|
getProjects(): Observable<Project[]> {
|
||||||
if (environment.useMockData) return this.mockService.getProjects();
|
if (environment.useMockData) return this.mockService.getProjects();
|
||||||
@@ -46,16 +149,18 @@ export class ApiService {
|
|||||||
// Categories
|
// Categories
|
||||||
getCategories(projectId: string): Observable<Category[]> {
|
getCategories(projectId: string): Observable<Category[]> {
|
||||||
if (environment.useMockData) return this.mockService.getCategories(projectId);
|
if (environment.useMockData) return this.mockService.getCategories(projectId);
|
||||||
return this.http.get<Category[]>(`${this.API_BASE}/projects/${projectId}/categories`).pipe(
|
return this.http.get<any[]>(`${this.API_BASE}/projects/${projectId}/categories`).pipe(
|
||||||
retry(2),
|
retry(2),
|
||||||
|
map(cats => cats.map(c => this.normalizeCategory(c))),
|
||||||
catchError(this.handleError)
|
catchError(this.handleError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategory(categoryId: string): Observable<Category> {
|
getCategory(categoryId: string): Observable<Category> {
|
||||||
if (environment.useMockData) return this.mockService.getCategory(categoryId);
|
if (environment.useMockData) return this.mockService.getCategory(categoryId);
|
||||||
return this.http.get<Category>(`${this.API_BASE}/categories/${categoryId}`).pipe(
|
return this.http.get<any>(`${this.API_BASE}/categories/${categoryId}`).pipe(
|
||||||
retry(2),
|
retry(2),
|
||||||
|
map(c => this.normalizeCategory(c)),
|
||||||
catchError(this.handleError)
|
catchError(this.handleError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -148,16 +253,21 @@ export class ApiService {
|
|||||||
params = params.set('tags', filters.tags.join(','));
|
params = params.set('tags', filters.tags.join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<ItemsListResponse>(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe(
|
return this.http.get<any>(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe(
|
||||||
retry(2),
|
retry(2),
|
||||||
|
map(resp => ({
|
||||||
|
...resp,
|
||||||
|
items: (resp.items || []).map((i: any) => this.normalizeItem(i))
|
||||||
|
})),
|
||||||
catchError(this.handleError)
|
catchError(this.handleError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem(itemId: string): Observable<Item> {
|
getItem(itemId: string): Observable<Item> {
|
||||||
if (environment.useMockData) return this.mockService.getItem(itemId);
|
if (environment.useMockData) return this.mockService.getItem(itemId);
|
||||||
return this.http.get<Item>(`${this.API_BASE}/items/${itemId}`).pipe(
|
return this.http.get<any>(`${this.API_BASE}/items/${itemId}`).pipe(
|
||||||
retry(2),
|
retry(2),
|
||||||
|
map(raw => this.normalizeItem(raw)),
|
||||||
catchError(this.handleError)
|
catchError(this.handleError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export class ValidationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateCurrency(value: string): string | null {
|
validateCurrency(value: string): string | null {
|
||||||
const validCurrencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH'];
|
const validCurrencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH', 'AMD'];
|
||||||
if (!validCurrencies.includes(value)) {
|
if (!validCurrencies.includes(value)) {
|
||||||
return `Currency must be one of: ${validCurrencies.join(', ')}`;
|
return `Currency must be one of: ${validCurrencies.join(', ')}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user