223 lines
6.6 KiB
TypeScript
223 lines
6.6 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
import { ApiResponse, PaginatedResponse, FilterOptions } from '../types';
|
|
|
|
export interface ApiServiceConfig {
|
|
baseURL: string;
|
|
defaultHeaders?: Record<string, string>;
|
|
timeout?: number;
|
|
}
|
|
|
|
export interface UseApiServiceReturn<T> {
|
|
data: T[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
total: number;
|
|
hasMore: boolean;
|
|
client: AxiosInstance;
|
|
|
|
// CRUD operations
|
|
getAll: (filters?: FilterOptions) => Promise<T[]>;
|
|
getById: (id: string) => Promise<T>;
|
|
create: (data: Partial<T>) => Promise<T>;
|
|
update: (id: string, data: Partial<T>) => Promise<T>;
|
|
delete: (id: string) => Promise<void>;
|
|
|
|
// Utility methods
|
|
clearError: () => void;
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
export const useApiService = <T extends { id: string }>(
|
|
config: ApiServiceConfig,
|
|
endpoint: string
|
|
): UseApiServiceReturn<T> => {
|
|
const [data, setData] = useState<T[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [total, setTotal] = useState(0);
|
|
const [hasMore, setHasMore] = useState(false);
|
|
|
|
// Create axios instance
|
|
const client = axios.create({
|
|
baseURL: config.baseURL,
|
|
timeout: config.timeout || 10000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...config.defaultHeaders,
|
|
},
|
|
});
|
|
|
|
// Add request interceptor for common headers
|
|
client.interceptors.request.use(
|
|
(config) => {
|
|
// Add user email header if available (common pattern in the codebase)
|
|
const userEmail = 'admin@example.com'; // This could come from a context or config
|
|
if (userEmail) {
|
|
config.headers['X-User-Email'] = userEmail;
|
|
}
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error)
|
|
);
|
|
|
|
// Add response interceptor for error handling
|
|
client.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
const errorMessage = error.response?.data?.message || error.message || 'An error occurred';
|
|
setError(errorMessage);
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
const clearError = useCallback(() => {
|
|
setError(null);
|
|
}, []);
|
|
|
|
const getAll = useCallback(async (filters: FilterOptions = {}): Promise<T[]> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
Object.entries(filters).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
params.append(key, value.toString());
|
|
}
|
|
});
|
|
|
|
const response = await client.get<PaginatedResponse<T> | ApiResponse<T[]>>(`${endpoint}?${params.toString()}`);
|
|
|
|
// Handle both paginated and simple array responses
|
|
if ('data' in response.data && Array.isArray(response.data.data)) {
|
|
// Paginated response
|
|
const paginatedData = response.data as PaginatedResponse<T>;
|
|
setData(paginatedData.data);
|
|
setTotal(paginatedData.total);
|
|
setHasMore(paginatedData.has_more || false);
|
|
return paginatedData.data;
|
|
} else if ('data' in response.data && Array.isArray(response.data.data)) {
|
|
// Simple array response wrapped in ApiResponse
|
|
const apiData = response.data as ApiResponse<T[]>;
|
|
setData(apiData.data);
|
|
setTotal(apiData.data.length);
|
|
setHasMore(false);
|
|
return apiData.data;
|
|
} else if (Array.isArray(response.data)) {
|
|
// Direct array response
|
|
setData(response.data);
|
|
setTotal(response.data.length);
|
|
setHasMore(false);
|
|
return response.data;
|
|
} else {
|
|
throw new Error('Invalid response format');
|
|
}
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.message || err.message || 'Failed to fetch data';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [client, endpoint]);
|
|
|
|
const getById = useCallback(async (id: string): Promise<T> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await client.get<ApiResponse<T> | T>(`${endpoint}/${id}`);
|
|
const item = 'data' in response.data ? response.data.data : response.data;
|
|
return item;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.message || err.message || 'Failed to fetch item';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [client, endpoint]);
|
|
|
|
const create = useCallback(async (itemData: Partial<T>): Promise<T> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await client.post<ApiResponse<T> | T>(endpoint, itemData);
|
|
const newItem = 'data' in response.data ? response.data.data : response.data;
|
|
|
|
// Update local data
|
|
setData(prev => [...prev, newItem]);
|
|
setTotal(prev => prev + 1);
|
|
|
|
return newItem;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.message || err.message || 'Failed to create item';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [client, endpoint]);
|
|
|
|
const update = useCallback(async (id: string, itemData: Partial<T>): Promise<T> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await client.put<ApiResponse<T> | T>(`${endpoint}/${id}`, itemData);
|
|
const updatedItem = 'data' in response.data ? response.data.data : response.data;
|
|
|
|
// Update local data
|
|
setData(prev => prev.map(item => item.id === id ? updatedItem : item));
|
|
|
|
return updatedItem;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.message || err.message || 'Failed to update item';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [client, endpoint]);
|
|
|
|
const deleteItem = useCallback(async (id: string): Promise<void> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await client.delete(`${endpoint}/${id}`);
|
|
|
|
// Update local data
|
|
setData(prev => prev.filter(item => item.id !== id));
|
|
setTotal(prev => prev - 1);
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.message || err.message || 'Failed to delete item';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [client, endpoint]);
|
|
|
|
const refresh = useCallback(async () => {
|
|
await getAll();
|
|
}, [getAll]);
|
|
|
|
return {
|
|
data,
|
|
loading,
|
|
error,
|
|
total,
|
|
hasMore,
|
|
client,
|
|
getAll,
|
|
getById,
|
|
create,
|
|
update,
|
|
delete: deleteItem,
|
|
clearError,
|
|
refresh,
|
|
};
|
|
}; |