diff --git a/src/app/models/category.model.ts b/src/app/models/category.model.ts index be7c020..bb11379 100644 --- a/src/app/models/category.model.ts +++ b/src/app/models/category.model.ts @@ -1,3 +1,5 @@ +import { ItemName } from './item.model'; + /** * Per-language translation content for a category or subcategory. * Stored under `translations['ru']`, `translations['en']`, etc. @@ -16,6 +18,15 @@ export interface Category { subcategories?: Subcategory[]; /** Optional translations keyed by language code: { ru: { name: '...' } } */ 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 { diff --git a/src/app/models/item.model.ts b/src/app/models/item.model.ts index ed57a4b..954731f 100644 --- a/src/app/models/item.model.ts +++ b/src/app/models/item.model.ts @@ -11,12 +11,15 @@ export interface ItemTranslation { export interface ItemName { language: string; value: string; + /** Backend typo — some responses use 'valuue' instead of 'value' */ + valuue?: string; } /** Localized description entry */ export interface ItemDescription { language: string; value: string; + valuue?: string; } /** Key-value attribute pair */ @@ -25,6 +28,39 @@ export interface ItemAttribute { 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 { id: string; name: string; @@ -48,6 +84,18 @@ export interface Item { comments?: Comment[]; /** Optional translations keyed by language code: { ru: { name: '...', simpleDescription: '...', description: [...] } } */ 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 { diff --git a/src/app/pages/item-editor/item-editor.component.ts b/src/app/pages/item-editor/item-editor.component.ts index 0f1d2fa..5e8c062 100644 --- a/src/app/pages/item-editor/item-editor.component.ts +++ b/src/app/pages/item-editor/item-editor.component.ts @@ -69,7 +69,7 @@ export class ItemEditorComponent implements OnInit { ruSimpleDesc = ''; ruDescFields: ItemDescriptionField[] = []; - currencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH']; + currencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH', 'AMD']; predefinedBadges: { label: string; value: string; color: string }[] = [ { label: 'New', value: 'new', color: '#009688' }, diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index c976dc4..a2be6b6 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable, Subject, throwError } from 'rxjs'; import { debounceTime, retry, catchError, map, groupBy, mergeMap } from 'rxjs/operators'; import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models'; +import { ItemName } from '../models/item.model'; import { MockDataService } from './mock-data.service'; import { ToastService } from './toast.service'; 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 getProjects(): Observable { if (environment.useMockData) return this.mockService.getProjects(); @@ -46,16 +149,18 @@ export class ApiService { // Categories getCategories(projectId: string): Observable { if (environment.useMockData) return this.mockService.getCategories(projectId); - return this.http.get(`${this.API_BASE}/projects/${projectId}/categories`).pipe( + return this.http.get(`${this.API_BASE}/projects/${projectId}/categories`).pipe( retry(2), + map(cats => cats.map(c => this.normalizeCategory(c))), catchError(this.handleError) ); } getCategory(categoryId: string): Observable { if (environment.useMockData) return this.mockService.getCategory(categoryId); - return this.http.get(`${this.API_BASE}/categories/${categoryId}`).pipe( + return this.http.get(`${this.API_BASE}/categories/${categoryId}`).pipe( retry(2), + map(c => this.normalizeCategory(c)), catchError(this.handleError) ); } @@ -148,16 +253,21 @@ export class ApiService { params = params.set('tags', filters.tags.join(',')); } - return this.http.get(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe( + return this.http.get(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe( retry(2), + map(resp => ({ + ...resp, + items: (resp.items || []).map((i: any) => this.normalizeItem(i)) + })), catchError(this.handleError) ); } getItem(itemId: string): Observable { if (environment.useMockData) return this.mockService.getItem(itemId); - return this.http.get(`${this.API_BASE}/items/${itemId}`).pipe( + return this.http.get(`${this.API_BASE}/items/${itemId}`).pipe( retry(2), + map(raw => this.normalizeItem(raw)), catchError(this.handleError) ); } diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts index a44daef..6a56afd 100644 --- a/src/app/services/validation.service.ts +++ b/src/app/services/validation.service.ts @@ -107,7 +107,7 @@ export class ValidationService { } validateCurrency(value: string): string | null { - const validCurrencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH']; + const validCurrencies = ['USD', 'EUR', 'RUB', 'GBP', 'UAH', 'AMD']; if (!validCurrencies.includes(value)) { return `Currency must be one of: ${validCurrencies.join(', ')}`; }