Files
market-backOfficce/src/app/services/api.service.ts

302 lines
10 KiB
TypeScript
Raw Normal View History

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-03-01 02:40:42 +04:00
import { Observable, Subject, throwError } from 'rxjs';
import { debounceTime, retry, catchError, map, groupBy, mergeMap } from 'rxjs/operators';
2026-01-19 23:17:07 +04:00
import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models';
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);
});
}
// 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);
return this.http.get<Category[]>(`${this.API_BASE}/projects/${projectId}/categories`).pipe(
retry(2),
catchError(this.handleError)
);
}
getCategory(categoryId: string): Observable<Category> {
if (environment.useMockData) return this.mockService.getCategory(categoryId);
return this.http.get<Category>(`${this.API_BASE}/categories/${categoryId}`).pipe(
retry(2),
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(','));
}
return this.http.get<ItemsListResponse>(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe(
retry(2),
catchError(this.handleError)
);
}
getItem(itemId: string): Observable<Item> {
if (environment.useMockData) return this.mockService.getItem(itemId);
return this.http.get<Item>(`${this.API_BASE}/items/${itemId}`).pipe(
retry(2),
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)
);
}
// 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-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[];
}