import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable, Subject, timer } from 'rxjs'; import { debounce, retry, catchError, tap, map } 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( map(cat => cat.subcategories || []) ); } 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(parentId: string, parentType: 'category' | 'subcategory', data: Partial): Observable { 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(endpoint, 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 { 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 }); throw { message: errorMessage, status: error.status, originalError: error }; } } interface SaveOperation { type: 'category' | 'subcategory' | 'item'; id: string; field: string; value: any; } interface ItemFilters { visible?: boolean; tags?: string[]; }