import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable, Subject, timer } from 'rxjs'; import { debounce, retry, catchError, tap } from 'rxjs/operators'; import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models'; import { MockDataService } from './mock-data.service'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class ApiService { private http = inject(HttpClient); private mockService = inject(MockDataService); private readonly API_BASE = environment.apiUrl; // Debounced save queue private saveQueue$ = new Subject(); constructor() { // Set up auto-save with 500ms debounce this.saveQueue$ .pipe(debounce(() => timer(500))) .subscribe(operation => { this.executeSave(operation); }); } // Projects getProjects(): Observable { if (environment.useMockData) return this.mockService.getProjects(); return this.http.get(`${this.API_BASE}/projects`).pipe( retry(2), catchError(this.handleError) ); } // Categories getCategories(projectId: string): Observable { if (environment.useMockData) return this.mockService.getCategories(projectId); return this.http.get(`${this.API_BASE}/projects/${projectId}/categories`).pipe( retry(2), 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( retry(2), catchError(this.handleError) ); } updateCategory(categoryId: string, data: Partial): Observable { if (environment.useMockData) return this.mockService.updateCategory(categoryId, data); return this.http.patch(`${this.API_BASE}/categories/${categoryId}`, data).pipe( retry(1), catchError(this.handleError) ); } createCategory(projectId: string, data: Partial): Observable { if (environment.useMockData) return this.mockService.createCategory(projectId, data); return this.http.post(`${this.API_BASE}/projects/${projectId}/categories`, data).pipe( catchError(this.handleError) ); } deleteCategory(categoryId: string): Observable { if (environment.useMockData) return this.mockService.deleteCategory(categoryId); return this.http.delete(`${this.API_BASE}/categories/${categoryId}`).pipe( catchError(this.handleError) ); } // Subcategories getSubcategories(categoryId: string): Observable { if (environment.useMockData) return this.mockService.getCategory(categoryId).pipe( tap(cat => cat.subcategories || []) ) as any; return this.http.get(`${this.API_BASE}/categories/${categoryId}/subcategories`).pipe( retry(2), catchError(this.handleError) ); } getSubcategory(subcategoryId: string): Observable { if (environment.useMockData) return this.mockService.getSubcategory(subcategoryId); return this.http.get(`${this.API_BASE}/subcategories/${subcategoryId}`).pipe( retry(2), catchError(this.handleError) ); } updateSubcategory(subcategoryId: string, data: Partial): Observable { if (environment.useMockData) return this.mockService.updateSubcategory(subcategoryId, data); return this.http.patch(`${this.API_BASE}/subcategories/${subcategoryId}`, data).pipe( retry(1), catchError(this.handleError) ); } createSubcategory(categoryId: string, data: Partial): Observable { if (environment.useMockData) return this.mockService.createSubcategory(categoryId, data); return this.http.post(`${this.API_BASE}/categories/${categoryId}/subcategories`, data).pipe( catchError(this.handleError) ); } deleteSubcategory(subcategoryId: string): Observable { if (environment.useMockData) return this.mockService.deleteSubcategory(subcategoryId); return this.http.delete(`${this.API_BASE}/subcategories/${subcategoryId}`).pipe( catchError(this.handleError) ); } // Items getItems(subcategoryId: string, page = 1, limit = 20, search?: string, filters?: ItemFilters): Observable { 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(`${this.API_BASE}/subcategories/${subcategoryId}/items`, { params }).pipe( retry(2), 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( retry(2), catchError(this.handleError) ); } updateItem(itemId: string, data: Partial): Observable { if (environment.useMockData) return this.mockService.updateItem(itemId, data); return this.http.patch(`${this.API_BASE}/items/${itemId}`, data).pipe( retry(1), catchError(this.handleError) ); } createItem(subcategoryId: string, data: Partial): Observable { if (environment.useMockData) return this.mockService.createItem(subcategoryId, data); return this.http.post(`${this.API_BASE}/subcategories/${subcategoryId}/items`, data).pipe( catchError(this.handleError) ); } deleteItem(itemId: string): Observable { if (environment.useMockData) return this.mockService.deleteItem(itemId); return this.http.delete(`${this.API_BASE}/items/${itemId}`).pipe( catchError(this.handleError) ); } bulkUpdateItems(itemIds: string[], data: Partial): Observable { if (environment.useMockData) return this.mockService.bulkUpdateItems(itemIds, data); return this.http.patch(`${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 queueSave(type: 'category' | 'subcategory' | 'item', id: string, field: string, value: any) { this.saveQueue$.next({ type, id, field, value }); } private executeSave(operation: SaveOperation) { const data = { [operation.field]: operation.value }; let request: Observable; 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({ next: () => console.log(`Saved ${operation.type} ${operation.id} - ${operation.field}`), error: (err) => console.error(`Failed to save ${operation.type}`, err) }); } private handleError(error: any): Observable { console.error('API Error:', error); throw error; } } interface SaveOperation { type: 'category' | 'subcategory' | 'item'; id: string; field: string; value: any; } interface ItemFilters { visible?: boolean; tags?: string[]; }