239 lines
8.1 KiB
TypeScript
239 lines
8.1 KiB
TypeScript
|
|
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<SaveOperation>();
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
// Set up auto-save with 500ms debounce
|
||
|
|
this.saveQueue$
|
||
|
|
.pipe(debounce(() => timer(500)))
|
||
|
|
.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[]> {
|
||
|
|
if (environment.useMockData) return this.mockService.getCategory(categoryId).pipe(
|
||
|
|
tap(cat => cat.subcategories || [])
|
||
|
|
) as any;
|
||
|
|
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)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
createSubcategory(categoryId: string, data: Partial<Subcategory>): Observable<Subcategory> {
|
||
|
|
if (environment.useMockData) return this.mockService.createSubcategory(categoryId, data);
|
||
|
|
return this.http.post<Subcategory>(`${this.API_BASE}/categories/${categoryId}/subcategories`, data).pipe(
|
||
|
|
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
|
||
|
|
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<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({
|
||
|
|
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<never> {
|
||
|
|
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[];
|
||
|
|
}
|