2026-03-01 02:40:42 +04:00
|
|
|
import { Injectable, inject, signal } from '@angular/core';
|
2026-01-19 23:17:07 +04:00
|
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
2026-06-22 01:44:17 +04:00
|
|
|
import { EMPTY, Observable, Subject, from, of, throwError } from 'rxjs';
|
|
|
|
|
import {
|
|
|
|
|
catchError,
|
|
|
|
|
concatMap,
|
|
|
|
|
debounceTime,
|
|
|
|
|
expand,
|
|
|
|
|
groupBy,
|
|
|
|
|
map,
|
|
|
|
|
mergeMap,
|
|
|
|
|
reduce,
|
|
|
|
|
retry,
|
|
|
|
|
toArray,
|
|
|
|
|
} from 'rxjs/operators';
|
2026-01-19 23:17:07 +04:00
|
|
|
import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models';
|
2026-03-24 02:47:31 +04:00
|
|
|
import { ItemName } from '../models/item.model';
|
2026-01-19 23:17:07 +04:00
|
|
|
import { MockDataService } from './mock-data.service';
|
2026-03-01 02:40:42 +04:00
|
|
|
import { ToastService } from './toast.service';
|
2026-01-19 23:17:07 +04:00
|
|
|
import { environment } from '../../environments/environment';
|
|
|
|
|
|
|
|
|
|
@Injectable({
|
|
|
|
|
providedIn: 'root'
|
|
|
|
|
})
|
|
|
|
|
export class ApiService {
|
|
|
|
|
private http = inject(HttpClient);
|
|
|
|
|
private mockService = inject(MockDataService);
|
2026-03-01 02:40:42 +04:00
|
|
|
private toast = inject(ToastService);
|
2026-01-19 23:17:07 +04:00
|
|
|
private readonly API_BASE = environment.apiUrl;
|
2026-03-01 02:40:42 +04:00
|
|
|
|
|
|
|
|
/** Whether a debounced save is in-flight */
|
|
|
|
|
saving = signal(false);
|
|
|
|
|
|
2026-01-19 23:17:07 +04:00
|
|
|
// Debounced save queue
|
|
|
|
|
private saveQueue$ = new Subject<SaveOperation>();
|
|
|
|
|
|
|
|
|
|
constructor() {
|
2026-03-01 02:40:42 +04:00
|
|
|
// Debounce per unique type+id+field so independent fields don't clobber each other
|
2026-01-19 23:17:07 +04:00
|
|
|
this.saveQueue$
|
2026-03-01 02:40:42 +04:00
|
|
|
.pipe(
|
|
|
|
|
groupBy(op => `${op.type}:${op.id}:${op.field}`),
|
|
|
|
|
mergeMap(group$ => group$.pipe(debounceTime(500)))
|
|
|
|
|
)
|
2026-01-19 23:17:07 +04:00
|
|
|
.subscribe(operation => {
|
|
|
|
|
this.executeSave(operation);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:47:31 +04:00
|
|
|
// ─── 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 23:17:07 +04:00
|
|
|
// Projects
|
|
|
|
|
getProjects(): Observable<Project[]> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.getProjects();
|
|
|
|
|
return this.http.get<Project[]>(`${this.API_BASE}/projects`).pipe(
|
|
|
|
|
retry(2),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Categories
|
|
|
|
|
getCategories(projectId: string): Observable<Category[]> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.getCategories(projectId);
|
2026-03-24 02:47:31 +04:00
|
|
|
return this.http.get<any[]>(`${this.API_BASE}/projects/${projectId}/categories`).pipe(
|
2026-01-19 23:17:07 +04:00
|
|
|
retry(2),
|
2026-03-24 02:47:31 +04:00
|
|
|
map(cats => cats.map(c => this.normalizeCategory(c))),
|
2026-01-19 23:17:07 +04:00
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCategory(categoryId: string): Observable<Category> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.getCategory(categoryId);
|
2026-03-24 02:47:31 +04:00
|
|
|
return this.http.get<any>(`${this.API_BASE}/categories/${categoryId}`).pipe(
|
2026-01-19 23:17:07 +04:00
|
|
|
retry(2),
|
2026-03-24 02:47:31 +04:00
|
|
|
map(c => this.normalizeCategory(c)),
|
2026-01-19 23:17:07 +04:00
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateCategory(categoryId: string, data: Partial<Category>): Observable<Category> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.updateCategory(categoryId, data);
|
|
|
|
|
return this.http.patch<Category>(`${this.API_BASE}/categories/${categoryId}`, data).pipe(
|
|
|
|
|
retry(1),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createCategory(projectId: string, data: Partial<Category>): Observable<Category> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.createCategory(projectId, data);
|
|
|
|
|
return this.http.post<Category>(`${this.API_BASE}/projects/${projectId}/categories`, data).pipe(
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deleteCategory(categoryId: string): Observable<void> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.deleteCategory(categoryId);
|
|
|
|
|
return this.http.delete<void>(`${this.API_BASE}/categories/${categoryId}`).pipe(
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subcategories
|
|
|
|
|
getSubcategories(categoryId: string): Observable<Subcategory[]> {
|
2026-01-22 00:41:13 +04:00
|
|
|
if (environment.useMockData) {
|
|
|
|
|
return this.mockService.getCategory(categoryId).pipe(
|
|
|
|
|
map(cat => cat.subcategories || [])
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-19 23:17:07 +04:00
|
|
|
return this.http.get<Subcategory[]>(`${this.API_BASE}/categories/${categoryId}/subcategories`).pipe(
|
|
|
|
|
retry(2),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSubcategory(subcategoryId: string): Observable<Subcategory> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.getSubcategory(subcategoryId);
|
|
|
|
|
return this.http.get<Subcategory>(`${this.API_BASE}/subcategories/${subcategoryId}`).pipe(
|
|
|
|
|
retry(2),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateSubcategory(subcategoryId: string, data: Partial<Subcategory>): Observable<Subcategory> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.updateSubcategory(subcategoryId, data);
|
|
|
|
|
return this.http.patch<Subcategory>(`${this.API_BASE}/subcategories/${subcategoryId}`, data).pipe(
|
|
|
|
|
retry(1),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 01:25:29 +04:00
|
|
|
createSubcategory(parentId: string, parentType: 'category' | 'subcategory', data: Partial<Subcategory>): Observable<Subcategory> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.createSubcategory(parentId, data);
|
|
|
|
|
const endpoint = parentType === 'category'
|
|
|
|
|
? `${this.API_BASE}/categories/${parentId}/subcategories`
|
|
|
|
|
: `${this.API_BASE}/subcategories/${parentId}/subcategories`;
|
|
|
|
|
return this.http.post<Subcategory>(endpoint, data).pipe(
|
2026-01-19 23:17:07 +04:00
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deleteSubcategory(subcategoryId: string): Observable<void> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.deleteSubcategory(subcategoryId);
|
|
|
|
|
return this.http.delete<void>(`${this.API_BASE}/subcategories/${subcategoryId}`).pipe(
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Items
|
|
|
|
|
getItems(subcategoryId: string, page = 1, limit = 20, search?: string, filters?: ItemFilters): Observable<ItemsListResponse> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.getItems(subcategoryId, page, limit, search, filters);
|
|
|
|
|
|
|
|
|
|
let params = new HttpParams()
|
|
|
|
|
.set('page', page.toString())
|
|
|
|
|
.set('limit', limit.toString());
|
|
|
|
|
|
|
|
|
|
if (search) {
|
|
|
|
|
params = params.set('search', search);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters?.visible !== undefined) {
|
|
|
|
|
params = params.set('visible', filters.visible.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters?.tags?.length) {
|
|
|
|
|
params = params.set('tags', filters.tags.join(','));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:47:31 +04:00
|
|
|
return this.http.get<any>(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe(
|
2026-01-19 23:17:07 +04:00
|
|
|
retry(2),
|
2026-03-24 02:47:31 +04:00
|
|
|
map(resp => ({
|
|
|
|
|
...resp,
|
|
|
|
|
items: (resp.items || []).map((i: any) => this.normalizeItem(i))
|
|
|
|
|
})),
|
2026-01-19 23:17:07 +04:00
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getItem(itemId: string): Observable<Item> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.getItem(itemId);
|
2026-03-24 02:47:31 +04:00
|
|
|
return this.http.get<any>(`${this.API_BASE}/items/${itemId}`).pipe(
|
2026-01-19 23:17:07 +04:00
|
|
|
retry(2),
|
2026-03-24 02:47:31 +04:00
|
|
|
map(raw => this.normalizeItem(raw)),
|
2026-01-19 23:17:07 +04:00
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateItem(itemId: string, data: Partial<Item>): Observable<Item> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.updateItem(itemId, data);
|
|
|
|
|
return this.http.patch<Item>(`${this.API_BASE}/items/${itemId}`, data).pipe(
|
|
|
|
|
retry(1),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createItem(subcategoryId: string, data: Partial<Item>): Observable<Item> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.createItem(subcategoryId, data);
|
|
|
|
|
return this.http.post<Item>(`${this.API_BASE}/subcategories/${subcategoryId}/items`, data).pipe(
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deleteItem(itemId: string): Observable<void> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.deleteItem(itemId);
|
|
|
|
|
return this.http.delete<void>(`${this.API_BASE}/items/${itemId}`).pipe(
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bulkUpdateItems(itemIds: string[], data: Partial<Item>): Observable<void> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.bulkUpdateItems(itemIds, data);
|
|
|
|
|
return this.http.patch<void>(`${this.API_BASE}/items/bulk`, { itemIds, data }).pipe(
|
|
|
|
|
retry(1),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 01:44:17 +04:00
|
|
|
setProjectVisibility(categories: Category[], visible: boolean): Observable<void> {
|
|
|
|
|
const { categoryIds, subcategoryIds } = this.collectVisibilityTargets(categories);
|
|
|
|
|
const requests = [
|
|
|
|
|
...categoryIds.map(id => () => this.updateCategory(id, { visible })),
|
|
|
|
|
...subcategoryIds.map(id => () => this.updateSubcategory(id, { visible })),
|
|
|
|
|
...subcategoryIds.map(id => () => this.updateSubcategoryItemsVisibility(id, visible)),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (!requests.length) {
|
|
|
|
|
return of(void 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return from(requests).pipe(
|
|
|
|
|
concatMap(request => request()),
|
|
|
|
|
toArray(),
|
|
|
|
|
map(() => void 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 23:17:07 +04:00
|
|
|
// Image upload
|
|
|
|
|
uploadImage(file: File): Observable<{ url: string }> {
|
|
|
|
|
if (environment.useMockData) return this.mockService.uploadImage(file);
|
|
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('image', file);
|
|
|
|
|
|
|
|
|
|
return this.http.post<{ url: string }>(`${this.API_BASE}/upload`, formData).pipe(
|
|
|
|
|
retry(1),
|
|
|
|
|
catchError(this.handleError)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debounced auto-save
|
2026-03-01 02:40:42 +04:00
|
|
|
queueSave(type: 'category' | 'subcategory' | 'item', id: string, field: string, value: unknown) {
|
|
|
|
|
this.saving.set(true);
|
2026-01-19 23:17:07 +04:00
|
|
|
this.saveQueue$.next({ type, id, field, value });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private executeSave(operation: SaveOperation) {
|
|
|
|
|
const data = { [operation.field]: operation.value };
|
|
|
|
|
|
|
|
|
|
let request: Observable<any>;
|
|
|
|
|
switch (operation.type) {
|
|
|
|
|
case 'category':
|
|
|
|
|
request = this.updateCategory(operation.id, data);
|
|
|
|
|
break;
|
|
|
|
|
case 'subcategory':
|
|
|
|
|
request = this.updateSubcategory(operation.id, data);
|
|
|
|
|
break;
|
|
|
|
|
case 'item':
|
|
|
|
|
request = this.updateItem(operation.id, data);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request.subscribe({
|
2026-03-01 02:40:42 +04:00
|
|
|
next: () => {
|
|
|
|
|
this.saving.set(false);
|
|
|
|
|
this.toast.success('Saved');
|
|
|
|
|
},
|
|
|
|
|
error: (err) => {
|
|
|
|
|
this.saving.set(false);
|
|
|
|
|
this.toast.error(err.message || 'Failed to save');
|
|
|
|
|
}
|
2026-01-19 23:17:07 +04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 01:44:17 +04:00
|
|
|
private updateSubcategoryItemsVisibility(subcategoryId: string, visible: boolean): Observable<void> {
|
|
|
|
|
return this.getAllSubcategoryItemIds(subcategoryId).pipe(
|
|
|
|
|
concatMap(itemIds => itemIds.length ? this.bulkUpdateItems(itemIds, { visible }) : of(void 0))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getAllSubcategoryItemIds(subcategoryId: string): Observable<string[]> {
|
|
|
|
|
const pageSize = 100;
|
|
|
|
|
|
|
|
|
|
return this.getItems(subcategoryId, 1, pageSize).pipe(
|
|
|
|
|
expand(response =>
|
|
|
|
|
response.hasMore ? this.getItems(subcategoryId, response.page + 1, pageSize) : EMPTY
|
|
|
|
|
),
|
|
|
|
|
reduce((itemIds, response) => {
|
|
|
|
|
itemIds.push(...response.items.map(item => item.id));
|
|
|
|
|
return itemIds;
|
|
|
|
|
}, [] as string[])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private collectVisibilityTargets(categories: Category[]) {
|
|
|
|
|
const categoryIds = categories.map(category => category.id);
|
|
|
|
|
const subcategoryIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
const visitSubcategories = (subcategories: Subcategory[]) => {
|
|
|
|
|
for (const subcategory of subcategories) {
|
|
|
|
|
subcategoryIds.push(subcategory.id);
|
|
|
|
|
|
|
|
|
|
if (subcategory.subcategories?.length) {
|
|
|
|
|
visitSubcategories(subcategory.subcategories);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const category of categories) {
|
|
|
|
|
visitSubcategories(category.subcategories || []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { categoryIds, subcategoryIds };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 02:40:42 +04:00
|
|
|
private handleError = (error: any): Observable<never> => {
|
2026-01-22 00:41:13 +04:00
|
|
|
let errorMessage = 'An unexpected error occurred';
|
|
|
|
|
|
|
|
|
|
if (error.error instanceof ErrorEvent) {
|
|
|
|
|
// Client-side or network error
|
|
|
|
|
errorMessage = `Network error: ${error.error.message}`;
|
|
|
|
|
} else if (error.status) {
|
|
|
|
|
// Backend returned an unsuccessful response code
|
|
|
|
|
switch (error.status) {
|
|
|
|
|
case 400:
|
|
|
|
|
errorMessage = error.error?.message || 'Invalid request';
|
|
|
|
|
break;
|
|
|
|
|
case 401:
|
|
|
|
|
errorMessage = 'Unauthorized. Please log in again.';
|
|
|
|
|
break;
|
|
|
|
|
case 403:
|
|
|
|
|
errorMessage = 'You do not have permission to perform this action';
|
|
|
|
|
break;
|
|
|
|
|
case 404:
|
|
|
|
|
errorMessage = 'Resource not found';
|
|
|
|
|
break;
|
|
|
|
|
case 409:
|
|
|
|
|
errorMessage = error.error?.message || 'Conflict: Resource already exists or has conflicts';
|
|
|
|
|
break;
|
|
|
|
|
case 422:
|
|
|
|
|
errorMessage = error.error?.message || 'Validation failed';
|
|
|
|
|
break;
|
|
|
|
|
case 500:
|
|
|
|
|
errorMessage = 'Server error. Please try again later.';
|
|
|
|
|
break;
|
|
|
|
|
case 503:
|
|
|
|
|
errorMessage = 'Service unavailable. Please try again later.';
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
errorMessage = error.error?.message || `Error: ${error.status} - ${error.statusText}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.error('API Error:', {
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
status: error.status,
|
|
|
|
|
error: error.error,
|
|
|
|
|
url: error.url
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-01 02:40:42 +04:00
|
|
|
return throwError(() => ({ message: errorMessage, status: error.status, originalError: error }));
|
|
|
|
|
};
|
2026-01-19 23:17:07 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SaveOperation {
|
|
|
|
|
type: 'category' | 'subcategory' | 'item';
|
|
|
|
|
id: string;
|
|
|
|
|
field: string;
|
2026-03-01 02:40:42 +04:00
|
|
|
value: unknown;
|
2026-01-19 23:17:07 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ItemFilters {
|
|
|
|
|
visible?: boolean;
|
|
|
|
|
tags?: string[];
|
|
|
|
|
}
|