ADD new frontend
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
// frontend/src/api/hooks/index.ts
|
||||
/**
|
||||
* Main Hooks Export
|
||||
*/
|
||||
|
||||
export { useAuth, useAuthHeaders } from './useAuth';
|
||||
export { useTenant } from './useTenant';
|
||||
export { useSales } from './useSales';
|
||||
export { useExternal } from './useExternal';
|
||||
export { useTraining } from './useTraining';
|
||||
export { useForecast } from './useForecast';
|
||||
export { useNotification } from './useNotification';
|
||||
export { useOnboarding, useOnboardingStep } from './useOnboarding';
|
||||
export { useInventory, useInventoryDashboard, useInventoryItem, useInventoryProducts } from './useInventory';
|
||||
export { useRecipes, useProduction } from './useRecipes';
|
||||
export {
|
||||
useCurrentProcurementPlan,
|
||||
useProcurementPlanByDate,
|
||||
useProcurementPlan,
|
||||
useProcurementPlans,
|
||||
usePlanRequirements,
|
||||
useCriticalRequirements,
|
||||
useProcurementDashboard,
|
||||
useGenerateProcurementPlan,
|
||||
useUpdatePlanStatus,
|
||||
useTriggerDailyScheduler,
|
||||
useProcurementHealth,
|
||||
useProcurementPlanDashboard,
|
||||
useProcurementPlanActions
|
||||
} from './useProcurement';
|
||||
|
||||
// Import hooks for combined usage
|
||||
import { useAuth } from './useAuth';
|
||||
import { useTenant } from './useTenant';
|
||||
import { useSales } from './useSales';
|
||||
import { useExternal } from './useExternal';
|
||||
import { useTraining } from './useTraining';
|
||||
import { useForecast } from './useForecast';
|
||||
import { useNotification } from './useNotification';
|
||||
import { useOnboarding } from './useOnboarding';
|
||||
import { useInventory } from './useInventory';
|
||||
|
||||
// Combined hook for common operations
|
||||
export const useApiHooks = () => {
|
||||
const auth = useAuth();
|
||||
const tenant = useTenant();
|
||||
const sales = useSales();
|
||||
const external = useExternal();
|
||||
const training = useTraining({ disablePolling: true }); // Disable polling by default
|
||||
const forecast = useForecast();
|
||||
const notification = useNotification();
|
||||
const onboarding = useOnboarding();
|
||||
const inventory = useInventory();
|
||||
|
||||
return {
|
||||
auth,
|
||||
tenant,
|
||||
sales,
|
||||
external,
|
||||
training,
|
||||
forecast,
|
||||
notification,
|
||||
onboarding,
|
||||
inventory
|
||||
};
|
||||
};
|
||||
@@ -1,205 +0,0 @@
|
||||
// frontend/src/api/hooks/useAuth.ts
|
||||
/**
|
||||
* Authentication Hooks
|
||||
* React hooks for authentication operations
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { authService } from '../services';
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
UserResponse,
|
||||
PasswordResetRequest,
|
||||
} from '../types';
|
||||
|
||||
// Token management
|
||||
const TOKEN_KEY = 'auth_token';
|
||||
const REFRESH_TOKEN_KEY = 'refresh_token';
|
||||
const USER_KEY = 'user_data';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [user, setUser] = useState<UserResponse | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Initialize auth state from localStorage
|
||||
useEffect(() => {
|
||||
const initializeAuth = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const userData = localStorage.getItem(USER_KEY);
|
||||
|
||||
if (token && userData) {
|
||||
setUser(JSON.parse(userData));
|
||||
setIsAuthenticated(true);
|
||||
|
||||
// Verify token is still valid
|
||||
try {
|
||||
const currentUser = await authService.getCurrentUser();
|
||||
setUser(currentUser);
|
||||
} catch (error) {
|
||||
// Token might be expired - let interceptors handle refresh
|
||||
// Only logout if refresh also fails (handled by ErrorRecoveryInterceptor)
|
||||
console.log('Token verification failed, interceptors will handle refresh if possible');
|
||||
|
||||
// Check if we have a refresh token - if not, logout immediately
|
||||
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
if (!refreshToken) {
|
||||
console.log('No refresh token available, logging out');
|
||||
logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization error:', error);
|
||||
logout();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (credentials: LoginRequest): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await authService.login(credentials);
|
||||
|
||||
// Store tokens and user data
|
||||
localStorage.setItem(TOKEN_KEY, response.access_token);
|
||||
if (response.refresh_token) {
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, response.refresh_token);
|
||||
}
|
||||
if (response.user) {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(response.user));
|
||||
setUser(response.user);
|
||||
}
|
||||
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Login failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (data: RegisterRequest): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await authService.register(data);
|
||||
|
||||
// Auto-login after successful registration
|
||||
if (response && response.user) {
|
||||
await login({ email: data.email, password: data.password });
|
||||
} else {
|
||||
// If response doesn't have user property, registration might still be successful
|
||||
// Try to login anyway in case the user was created but response format is different
|
||||
await login({ email: data.email, password: data.password });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Registration failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [login]);
|
||||
|
||||
const logout = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
// Call logout endpoint if authenticated
|
||||
if (isAuthenticated) {
|
||||
await authService.logout();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
// Clear local state regardless of API call success
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
setError(null);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const updateProfile = useCallback(async (data: Partial<UserResponse>): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const updatedUser = await authService.updateProfile(data);
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(updatedUser));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Profile update failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const requestPasswordReset = useCallback(async (data: PasswordResetRequest): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
await authService.requestPasswordReset(data);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Password reset request failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const changePassword = useCallback(async (currentPassword: string, newPassword: string): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
await authService.changePassword(currentPassword, newPassword);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Password change failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
error,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
updateProfile,
|
||||
requestPasswordReset,
|
||||
changePassword,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for getting authentication headers
|
||||
export const useAuthHeaders = () => {
|
||||
const getAuthHeaders = useCallback(() => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}, []);
|
||||
|
||||
return { getAuthHeaders };
|
||||
};
|
||||
@@ -1,238 +0,0 @@
|
||||
// frontend/src/api/hooks/useExternal.ts
|
||||
/**
|
||||
* External Data Management Hooks
|
||||
* Handles weather and traffic data operations
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { externalService } from '../services/external.service';
|
||||
import type { WeatherData, TrafficData, WeatherForecast, HourlyForecast } from '../services/external.service';
|
||||
|
||||
export const useExternal = () => {
|
||||
const [weatherData, setWeatherData] = useState<WeatherData | null>(null);
|
||||
const [trafficData, setTrafficData] = useState<TrafficData | null>(null);
|
||||
const [weatherForecast, setWeatherForecast] = useState<WeatherForecast[]>([]);
|
||||
const [hourlyForecast, setHourlyForecast] = useState<HourlyForecast[]>([]);
|
||||
const [trafficForecast, setTrafficForecast] = useState<TrafficData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Get Current Weather
|
||||
*/
|
||||
const getCurrentWeather = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number
|
||||
): Promise<WeatherData> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const weather = await externalService.getCurrentWeather(tenantId, lat, lon);
|
||||
setWeatherData(weather);
|
||||
|
||||
return weather;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get weather data';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Weather Forecast
|
||||
*/
|
||||
const getWeatherForecast = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
days: number = 7
|
||||
): Promise<WeatherForecast[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const forecast = await externalService.getWeatherForecast(tenantId, lat, lon, days);
|
||||
setWeatherForecast(forecast);
|
||||
|
||||
return forecast;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get weather forecast';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Hourly Weather Forecast
|
||||
*/
|
||||
const getHourlyWeatherForecast = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
hours: number = 48
|
||||
): Promise<HourlyForecast[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const forecast = await externalService.getHourlyWeatherForecast(tenantId, lat, lon, hours);
|
||||
setHourlyForecast(forecast);
|
||||
|
||||
return forecast;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get hourly weather forecast';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Historical Weather Data
|
||||
*/
|
||||
const getHistoricalWeather = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): Promise<WeatherData[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await externalService.getHistoricalWeather(tenantId, lat, lon, startDate, endDate);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get historical weather';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Current Traffic
|
||||
*/
|
||||
const getCurrentTraffic = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number
|
||||
): Promise<TrafficData> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const traffic = await externalService.getCurrentTraffic(tenantId, lat, lon);
|
||||
setTrafficData(traffic);
|
||||
|
||||
return traffic;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get traffic data';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Traffic Forecast
|
||||
*/
|
||||
const getTrafficForecast = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
hours: number = 24
|
||||
): Promise<TrafficData[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const forecast = await externalService.getTrafficForecast(tenantId, lat, lon, hours);
|
||||
setTrafficForecast(forecast);
|
||||
|
||||
return forecast;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get traffic forecast';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Historical Traffic Data
|
||||
*/
|
||||
const getHistoricalTraffic = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): Promise<TrafficData[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await externalService.getHistoricalTraffic(tenantId, lat, lon, startDate, endDate);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get historical traffic';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Test External Services Connectivity
|
||||
*/
|
||||
const testConnectivity = useCallback(async (tenantId: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const results = await externalService.testConnectivity(tenantId);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to test connectivity';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
weatherData,
|
||||
trafficData,
|
||||
weatherForecast,
|
||||
hourlyForecast,
|
||||
trafficForecast,
|
||||
isLoading,
|
||||
error,
|
||||
getCurrentWeather,
|
||||
getWeatherForecast,
|
||||
getHourlyWeatherForecast,
|
||||
getHistoricalWeather,
|
||||
getCurrentTraffic,
|
||||
getTrafficForecast,
|
||||
getHistoricalTraffic,
|
||||
testConnectivity,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
// frontend/src/api/hooks/useForecast.ts
|
||||
/**
|
||||
* Forecasting Operations Hooks
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { forecastingService } from '../services';
|
||||
import type {
|
||||
SingleForecastRequest,
|
||||
BatchForecastRequest,
|
||||
ForecastResponse,
|
||||
BatchForecastResponse,
|
||||
ForecastAlert,
|
||||
QuickForecast,
|
||||
} from '../types';
|
||||
|
||||
export const useForecast = () => {
|
||||
const [forecasts, setForecasts] = useState<ForecastResponse[]>([]);
|
||||
const [batchForecasts, setBatchForecasts] = useState<BatchForecastResponse[]>([]);
|
||||
const [quickForecasts, setQuickForecasts] = useState<QuickForecast[]>([]);
|
||||
const [alerts, setAlerts] = useState<ForecastAlert[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const createSingleForecast = useCallback(async (
|
||||
tenantId: string,
|
||||
request: SingleForecastRequest
|
||||
): Promise<ForecastResponse[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const newForecasts = await forecastingService.createSingleForecast(tenantId, request);
|
||||
setForecasts(prev => [...newForecasts, ...(prev || [])]);
|
||||
|
||||
return newForecasts;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create forecast';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createBatchForecast = useCallback(async (
|
||||
tenantId: string,
|
||||
request: BatchForecastRequest
|
||||
): Promise<BatchForecastResponse> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const batchForecast = await forecastingService.createBatchForecast(tenantId, request);
|
||||
setBatchForecasts(prev => [batchForecast, ...(prev || [])]);
|
||||
|
||||
return batchForecast;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create batch forecast';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getForecasts = useCallback(async (tenantId: string): Promise<ForecastResponse[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await forecastingService.getForecasts(tenantId);
|
||||
setForecasts(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get forecasts';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getBatchForecastStatus = useCallback(async (
|
||||
tenantId: string,
|
||||
batchId: string
|
||||
): Promise<BatchForecastResponse> => {
|
||||
try {
|
||||
const batchForecast = await forecastingService.getBatchForecastStatus(tenantId, batchId);
|
||||
|
||||
// Update batch forecast in state
|
||||
setBatchForecasts(prev => (prev || []).map(bf =>
|
||||
bf.id === batchId ? batchForecast : bf
|
||||
));
|
||||
|
||||
return batchForecast;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get batch forecast status';
|
||||
setError(message);
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getQuickForecasts = useCallback(async (tenantId: string): Promise<QuickForecast[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const quickForecastData = await forecastingService.getQuickForecasts(tenantId);
|
||||
setQuickForecasts(quickForecastData);
|
||||
|
||||
return quickForecastData;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get quick forecasts';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getForecastAlerts = useCallback(async (tenantId: string): Promise<any> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await forecastingService.getForecastAlerts(tenantId);
|
||||
|
||||
// Handle different response formats
|
||||
if (response && 'data' in response && response.data) {
|
||||
// Standard paginated format: { data: [...], pagination: {...} }
|
||||
setAlerts(response.data);
|
||||
return { alerts: response.data, ...response };
|
||||
} else if (response && Array.isArray(response)) {
|
||||
// Direct array format
|
||||
setAlerts(response);
|
||||
return { alerts: response };
|
||||
} else if (Array.isArray(response)) {
|
||||
// Direct array format
|
||||
setAlerts(response);
|
||||
return { alerts: response };
|
||||
} else {
|
||||
// Unknown format - return empty
|
||||
setAlerts([]);
|
||||
return { alerts: [] };
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get forecast alerts';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const acknowledgeForecastAlert = useCallback(async (
|
||||
tenantId: string,
|
||||
alertId: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const acknowledgedAlert = await forecastingService.acknowledgeForecastAlert(tenantId, alertId);
|
||||
setAlerts(prev => (prev || []).map(alert =>
|
||||
alert.id === alertId ? acknowledgedAlert : alert
|
||||
));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to acknowledge alert';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const exportForecasts = useCallback(async (
|
||||
tenantId: string,
|
||||
format: 'csv' | 'excel' | 'json',
|
||||
params?: {
|
||||
inventory_product_id?: string; // Primary way to filter by product
|
||||
product_name?: string; // For backward compatibility
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const blob = await forecastingService.exportForecasts(tenantId, format, params);
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `forecasts.${format}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Export failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
forecasts,
|
||||
batchForecasts,
|
||||
quickForecasts,
|
||||
alerts,
|
||||
isLoading,
|
||||
error,
|
||||
createSingleForecast,
|
||||
createBatchForecast,
|
||||
getForecasts,
|
||||
getBatchForecastStatus,
|
||||
getQuickForecasts,
|
||||
getForecastAlerts,
|
||||
acknowledgeForecastAlert,
|
||||
exportForecasts,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
@@ -1,536 +0,0 @@
|
||||
// frontend/src/api/hooks/useInventory.ts
|
||||
/**
|
||||
* Inventory Management React Hook
|
||||
* Provides comprehensive state management for inventory operations
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import {
|
||||
inventoryService,
|
||||
InventoryItem,
|
||||
StockLevel,
|
||||
StockMovement,
|
||||
InventorySearchParams,
|
||||
CreateInventoryItemRequest,
|
||||
UpdateInventoryItemRequest,
|
||||
StockAdjustmentRequest,
|
||||
PaginatedResponse,
|
||||
InventoryDashboardData
|
||||
} from '../services/inventory.service';
|
||||
import type { ProductInfo } from '../types';
|
||||
|
||||
import { useTenantId } from '../../hooks/useTenantId';
|
||||
|
||||
// ========== HOOK INTERFACES ==========
|
||||
|
||||
interface UseInventoryReturn {
|
||||
// State
|
||||
items: InventoryItem[];
|
||||
stockLevels: Record<string, StockLevel>;
|
||||
movements: StockMovement[];
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
// Actions
|
||||
loadItems: (params?: InventorySearchParams) => Promise<void>;
|
||||
loadItem: (itemId: string) => Promise<InventoryItem | null>;
|
||||
createItem: (data: CreateInventoryItemRequest) => Promise<InventoryItem | null>;
|
||||
updateItem: (itemId: string, data: UpdateInventoryItemRequest) => Promise<InventoryItem | null>;
|
||||
deleteItem: (itemId: string) => Promise<boolean>;
|
||||
|
||||
// Stock operations
|
||||
loadStockLevels: () => Promise<void>;
|
||||
adjustStock: (itemId: string, adjustment: StockAdjustmentRequest) => Promise<StockMovement | null>;
|
||||
loadMovements: (params?: any) => Promise<void>;
|
||||
|
||||
|
||||
// Dashboard
|
||||
loadDashboard: () => Promise<void>;
|
||||
|
||||
// Utility
|
||||
searchItems: (query: string) => Promise<InventoryItem[]>;
|
||||
refresh: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
interface UseInventoryDashboardReturn {
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseInventoryItemReturn {
|
||||
item: InventoryItem | null;
|
||||
stockLevel: StockLevel | null;
|
||||
recentMovements: StockMovement[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
updateItem: (data: UpdateInventoryItemRequest) => Promise<boolean>;
|
||||
adjustStock: (adjustment: StockAdjustmentRequest) => Promise<boolean>;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
// ========== MAIN INVENTORY HOOK ==========
|
||||
|
||||
export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
const { tenantId } = useTenantId();
|
||||
|
||||
// State
|
||||
const [items, setItems] = useState<InventoryItem[]>([]);
|
||||
const [stockLevels, setStockLevels] = useState<Record<string, StockLevel>>({});
|
||||
const [movements, setMovements] = useState<StockMovement[]>([]);
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => setError(null), []);
|
||||
|
||||
// Load inventory items
|
||||
const loadItems = useCallback(async (params?: InventorySearchParams) => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await inventoryService.getInventoryItems(tenantId, params);
|
||||
console.log('🔄 useInventory: Loaded items:', response.items);
|
||||
setItems(response.items || []); // Ensure it's always an array
|
||||
setPagination({
|
||||
page: response.page || 1,
|
||||
limit: response.limit || 20,
|
||||
total: response.total || 0,
|
||||
totalPages: response.total_pages || 0
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('❌ useInventory: Error loading items:', err);
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading inventory items';
|
||||
|
||||
setError(errorMessage);
|
||||
setItems([]); // Set empty array on error
|
||||
|
||||
// Show appropriate error message
|
||||
if (err.response?.status === 401) {
|
||||
console.error('❌ useInventory: Authentication failed');
|
||||
} else if (err.response?.status === 403) {
|
||||
toast.error('No tienes permisos para acceder a este inventario');
|
||||
} else {
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load single item
|
||||
const loadItem = useCallback(async (itemId: string): Promise<InventoryItem | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
try {
|
||||
const item = await inventoryService.getInventoryItem(tenantId, itemId);
|
||||
|
||||
// Update in local state if it exists
|
||||
setItems(prev => prev.map(i => i.id === itemId ? item : i));
|
||||
|
||||
return item;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading item';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Create item
|
||||
const createItem = useCallback(async (data: CreateInventoryItemRequest): Promise<InventoryItem | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const newItem = await inventoryService.createInventoryItem(tenantId, data);
|
||||
setItems(prev => [newItem, ...prev]);
|
||||
toast.success(`Created ${newItem.name} successfully`);
|
||||
return newItem;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error creating item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Update item
|
||||
const updateItem = useCallback(async (
|
||||
itemId: string,
|
||||
data: UpdateInventoryItemRequest
|
||||
): Promise<InventoryItem | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
try {
|
||||
const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data);
|
||||
setItems(prev => prev.map(i => i.id === itemId ? updatedItem : i));
|
||||
toast.success(`Updated ${updatedItem.name} successfully`);
|
||||
return updatedItem;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Delete item
|
||||
const deleteItem = useCallback(async (itemId: string): Promise<boolean> => {
|
||||
if (!tenantId) return false;
|
||||
|
||||
try {
|
||||
await inventoryService.deleteInventoryItem(tenantId, itemId);
|
||||
setItems(prev => prev.filter(i => i.id !== itemId));
|
||||
toast.success('Item deleted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error deleting item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load stock levels
|
||||
const loadStockLevels = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const levels = await inventoryService.getAllStockLevels(tenantId);
|
||||
const levelMap = levels.reduce((acc, level) => {
|
||||
acc[level.item_id] = level;
|
||||
return acc;
|
||||
}, {} as Record<string, StockLevel>);
|
||||
setStockLevels(levelMap);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading stock levels:', err);
|
||||
// Don't show toast error for this as it's not critical for forecast page
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Adjust stock
|
||||
const adjustStock = useCallback(async (
|
||||
itemId: string,
|
||||
adjustment: StockAdjustmentRequest
|
||||
): Promise<StockMovement | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
try {
|
||||
const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment);
|
||||
|
||||
// Update local movements
|
||||
setMovements(prev => [movement, ...prev.slice(0, 49)]); // Keep last 50
|
||||
|
||||
// Reload stock level for this item
|
||||
const updatedLevel = await inventoryService.getStockLevel(tenantId, itemId);
|
||||
setStockLevels(prev => ({ ...prev, [itemId]: updatedLevel }));
|
||||
|
||||
toast.success('Stock adjusted successfully');
|
||||
return movement;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load movements
|
||||
const loadMovements = useCallback(async (params?: any) => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const response = await inventoryService.getStockMovements(tenantId, params);
|
||||
setMovements(response.items);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading movements:', err);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
|
||||
// Load dashboard
|
||||
const loadDashboard = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const data = await inventoryService.getDashboardData(tenantId);
|
||||
setDashboardData(data);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading dashboard:', err);
|
||||
// Don't show toast error for this as it's not critical for forecast page
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Search items
|
||||
const searchItems = useCallback(async (query: string): Promise<InventoryItem[]> => {
|
||||
if (!tenantId || !query.trim()) return [];
|
||||
|
||||
try {
|
||||
return await inventoryService.searchItems(tenantId, query);
|
||||
} catch (err: any) {
|
||||
console.error('Error searching items:', err);
|
||||
return [];
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Refresh all data
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([
|
||||
loadItems(),
|
||||
loadStockLevels(),
|
||||
loadDashboard()
|
||||
]);
|
||||
}, [loadItems, loadStockLevels, loadDashboard]);
|
||||
|
||||
// Auto-load on mount
|
||||
useEffect(() => {
|
||||
if (autoLoad && tenantId) {
|
||||
refresh();
|
||||
}
|
||||
}, [autoLoad, tenantId, refresh]);
|
||||
|
||||
return {
|
||||
// State
|
||||
items,
|
||||
stockLevels,
|
||||
movements,
|
||||
dashboardData,
|
||||
isLoading,
|
||||
error,
|
||||
pagination,
|
||||
|
||||
// Actions
|
||||
loadItems,
|
||||
loadItem,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
|
||||
// Stock operations
|
||||
loadStockLevels,
|
||||
adjustStock,
|
||||
loadMovements,
|
||||
|
||||
// Dashboard
|
||||
loadDashboard,
|
||||
|
||||
// Utility
|
||||
searchItems,
|
||||
refresh,
|
||||
clearError
|
||||
};
|
||||
};
|
||||
|
||||
// ========== DASHBOARD HOOK ==========
|
||||
|
||||
export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
|
||||
const { tenantId } = useTenantId();
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const dashboard = await inventoryService.getDashboardData(tenantId);
|
||||
|
||||
setDashboardData(dashboard);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading dashboard';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId) {
|
||||
refresh();
|
||||
}
|
||||
}, [tenantId, refresh]);
|
||||
|
||||
return {
|
||||
dashboardData,
|
||||
isLoading,
|
||||
error,
|
||||
refresh
|
||||
};
|
||||
};
|
||||
|
||||
// ========== SINGLE ITEM HOOK ==========
|
||||
|
||||
export const useInventoryItem = (itemId: string): UseInventoryItemReturn => {
|
||||
const { tenantId } = useTenantId();
|
||||
const [item, setItem] = useState<InventoryItem | null>(null);
|
||||
const [stockLevel, setStockLevel] = useState<StockLevel | null>(null);
|
||||
const [recentMovements, setRecentMovements] = useState<StockMovement[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!tenantId || !itemId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [itemData, stockData, movementsData] = await Promise.all([
|
||||
inventoryService.getInventoryItem(tenantId, itemId),
|
||||
inventoryService.getStockLevel(tenantId, itemId),
|
||||
inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 })
|
||||
]);
|
||||
|
||||
setItem(itemData);
|
||||
setStockLevel(stockData);
|
||||
setRecentMovements(movementsData.items);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading item';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId, itemId]);
|
||||
|
||||
const updateItem = useCallback(async (data: UpdateInventoryItemRequest): Promise<boolean> => {
|
||||
if (!tenantId || !itemId) return false;
|
||||
|
||||
try {
|
||||
const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data);
|
||||
setItem(updatedItem);
|
||||
toast.success('Item updated successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [tenantId, itemId]);
|
||||
|
||||
const adjustStock = useCallback(async (adjustment: StockAdjustmentRequest): Promise<boolean> => {
|
||||
if (!tenantId || !itemId) return false;
|
||||
|
||||
try {
|
||||
const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment);
|
||||
|
||||
// Refresh data
|
||||
const [updatedStock, updatedMovements] = await Promise.all([
|
||||
inventoryService.getStockLevel(tenantId, itemId),
|
||||
inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 })
|
||||
]);
|
||||
|
||||
setStockLevel(updatedStock);
|
||||
setRecentMovements(updatedMovements.items);
|
||||
|
||||
toast.success('Stock adjusted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [tenantId, itemId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId && itemId) {
|
||||
refresh();
|
||||
}
|
||||
}, [tenantId, itemId, refresh]);
|
||||
|
||||
return {
|
||||
item,
|
||||
stockLevel,
|
||||
recentMovements,
|
||||
isLoading,
|
||||
error,
|
||||
updateItem,
|
||||
adjustStock,
|
||||
refresh
|
||||
};
|
||||
};
|
||||
|
||||
// ========== SIMPLE PRODUCTS HOOK FOR FORECASTING ==========
|
||||
|
||||
export const useInventoryProducts = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Get Products List for Forecasting
|
||||
*/
|
||||
const getProductsList = useCallback(async (tenantId: string): Promise<ProductInfo[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const products = await inventoryService.getProductsList(tenantId);
|
||||
|
||||
return products;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get products list';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Product by ID
|
||||
*/
|
||||
const getProductById = useCallback(async (tenantId: string, productId: string): Promise<ProductInfo | null> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const product = await inventoryService.getProductById(tenantId, productId);
|
||||
|
||||
return product;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get product';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
getProductsList,
|
||||
getProductById,
|
||||
};
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
// frontend/src/api/hooks/useNotification.ts
|
||||
/**
|
||||
* Notification Operations Hooks
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { notificationService } from '../services';
|
||||
import type {
|
||||
NotificationCreate,
|
||||
NotificationResponse,
|
||||
NotificationTemplate,
|
||||
NotificationStats,
|
||||
BulkNotificationRequest,
|
||||
} from '../types';
|
||||
|
||||
export const useNotification = () => {
|
||||
const [notifications, setNotifications] = useState<NotificationResponse[]>([]);
|
||||
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
|
||||
const [stats, setStats] = useState<NotificationStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const sendNotification = useCallback(async (
|
||||
tenantId: string,
|
||||
notification: NotificationCreate
|
||||
): Promise<NotificationResponse> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const sentNotification = await notificationService.sendNotification(tenantId, notification);
|
||||
setNotifications(prev => [sentNotification, ...prev]);
|
||||
|
||||
return sentNotification;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to send notification';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sendBulkNotifications = useCallback(async (
|
||||
tenantId: string,
|
||||
request: BulkNotificationRequest
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await notificationService.sendBulkNotifications(tenantId, request);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to send bulk notifications';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getNotifications = useCallback(async (tenantId: string): Promise<NotificationResponse[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await notificationService.getNotifications(tenantId);
|
||||
setNotifications(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get notifications';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTemplates = useCallback(async (tenantId: string): Promise<NotificationTemplate[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await notificationService.getTemplates(tenantId);
|
||||
setTemplates(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get templates';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createTemplate = useCallback(async (
|
||||
tenantId: string,
|
||||
template: Omit<NotificationTemplate, 'id' | 'tenant_id' | 'created_at' | 'updated_at'>
|
||||
): Promise<NotificationTemplate> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const newTemplate = await notificationService.createTemplate(tenantId, template);
|
||||
setTemplates(prev => [newTemplate, ...prev]);
|
||||
|
||||
return newTemplate;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create template';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getNotificationStats = useCallback(async (tenantId: string): Promise<NotificationStats> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const notificationStats = await notificationService.getNotificationStats(tenantId);
|
||||
setStats(notificationStats);
|
||||
|
||||
return notificationStats;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get notification stats';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
templates,
|
||||
stats,
|
||||
isLoading,
|
||||
error,
|
||||
sendNotification,
|
||||
sendBulkNotifications,
|
||||
getNotifications,
|
||||
getTemplates,
|
||||
createTemplate,
|
||||
getNotificationStats,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
// frontend/src/api/hooks/useOnboarding.ts
|
||||
/**
|
||||
* Onboarding Hook
|
||||
* React hook for managing user onboarding flow and progress
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { onboardingService } from '../services/onboarding.service';
|
||||
import type { UserProgress, UpdateStepRequest } from '../services/onboarding.service';
|
||||
|
||||
export interface UseOnboardingReturn {
|
||||
progress: UserProgress | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
currentStep: string | null;
|
||||
nextStep: string | null;
|
||||
completionPercentage: number;
|
||||
isFullyComplete: boolean;
|
||||
|
||||
// Actions
|
||||
updateStep: (data: UpdateStepRequest) => Promise<void>;
|
||||
completeStep: (stepName: string, data?: Record<string, any>) => Promise<void>;
|
||||
resetStep: (stepName: string) => Promise<void>;
|
||||
getNextStep: () => Promise<string>;
|
||||
completeOnboarding: () => Promise<void>;
|
||||
canAccessStep: (stepName: string) => Promise<boolean>;
|
||||
refreshProgress: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useOnboarding = (): UseOnboardingReturn => {
|
||||
const [progress, setProgress] = useState<UserProgress | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Derived state
|
||||
const currentStep = progress?.current_step || null;
|
||||
const nextStep = progress?.next_step || null;
|
||||
const completionPercentage = progress?.completion_percentage || 0;
|
||||
const isFullyComplete = progress?.fully_completed || false;
|
||||
|
||||
// Load initial progress
|
||||
const loadProgress = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const userProgress = await onboardingService.getUserProgress();
|
||||
setProgress(userProgress);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load onboarding progress';
|
||||
setError(message);
|
||||
console.error('Onboarding progress load error:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update step
|
||||
const updateStep = async (data: UpdateStepRequest) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updatedProgress = await onboardingService.updateStep(data);
|
||||
setProgress(updatedProgress);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to update step';
|
||||
setError(message);
|
||||
throw err; // Re-throw so calling component can handle it
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Complete step with data
|
||||
const completeStep = async (stepName: string, data?: Record<string, any>) => {
|
||||
await updateStep({
|
||||
step_name: stepName,
|
||||
completed: true,
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
// Reset step
|
||||
const resetStep = async (stepName: string) => {
|
||||
await updateStep({
|
||||
step_name: stepName,
|
||||
completed: false
|
||||
});
|
||||
};
|
||||
|
||||
// Get next step
|
||||
const getNextStep = async (): Promise<string> => {
|
||||
try {
|
||||
const result = await onboardingService.getNextStep();
|
||||
return result.step;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to get next step';
|
||||
setError(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Complete entire onboarding
|
||||
const completeOnboarding = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await onboardingService.completeOnboarding();
|
||||
await loadProgress(); // Refresh progress after completion
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to complete onboarding';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if user can access step
|
||||
const canAccessStep = async (stepName: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await onboardingService.canAccessStep(stepName);
|
||||
return result.can_access;
|
||||
} catch (err) {
|
||||
console.error('Can access step check failed:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Refresh progress
|
||||
const refreshProgress = async () => {
|
||||
await loadProgress();
|
||||
};
|
||||
|
||||
// Load progress on mount
|
||||
useEffect(() => {
|
||||
loadProgress();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
progress,
|
||||
isLoading,
|
||||
error,
|
||||
currentStep,
|
||||
nextStep,
|
||||
completionPercentage,
|
||||
isFullyComplete,
|
||||
updateStep,
|
||||
completeStep,
|
||||
resetStep,
|
||||
getNextStep,
|
||||
completeOnboarding,
|
||||
canAccessStep,
|
||||
refreshProgress,
|
||||
};
|
||||
};
|
||||
|
||||
// Helper hook for specific steps
|
||||
export const useOnboardingStep = (stepName: string) => {
|
||||
const onboarding = useOnboarding();
|
||||
|
||||
const stepStatus = onboarding.progress?.steps.find(
|
||||
step => step.step_name === stepName
|
||||
);
|
||||
|
||||
const isCompleted = stepStatus?.completed || false;
|
||||
const stepData = stepStatus?.data || {};
|
||||
const completedAt = stepStatus?.completed_at;
|
||||
|
||||
const completeThisStep = async (data?: Record<string, any>) => {
|
||||
await onboarding.completeStep(stepName, data);
|
||||
};
|
||||
|
||||
const resetThisStep = async () => {
|
||||
await onboarding.resetStep(stepName);
|
||||
};
|
||||
|
||||
const canAccessThisStep = async (): Promise<boolean> => {
|
||||
return await onboarding.canAccessStep(stepName);
|
||||
};
|
||||
|
||||
return {
|
||||
...onboarding,
|
||||
stepName,
|
||||
isCompleted,
|
||||
stepData,
|
||||
completedAt,
|
||||
completeThisStep,
|
||||
resetThisStep,
|
||||
canAccessThisStep,
|
||||
};
|
||||
};
|
||||
@@ -1,337 +0,0 @@
|
||||
// frontend/src/api/hooks/usePOS.ts
|
||||
/**
|
||||
* React hooks for POS Integration functionality
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
posService,
|
||||
POSConfiguration,
|
||||
CreatePOSConfigurationRequest,
|
||||
UpdatePOSConfigurationRequest,
|
||||
POSTransaction,
|
||||
POSSyncLog,
|
||||
POSAnalytics,
|
||||
SyncRequest
|
||||
} from '../services/pos.service';
|
||||
import { useTenantId } from './useTenant';
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION HOOKS
|
||||
// ============================================================================
|
||||
|
||||
export const usePOSConfigurations = (params?: {
|
||||
pos_system?: string;
|
||||
is_active?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['pos-configurations', tenantId, params],
|
||||
queryFn: () => posService.getConfigurations(tenantId, params),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
export const usePOSConfiguration = (configId?: string) => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['pos-configuration', tenantId, configId],
|
||||
queryFn: () => posService.getConfiguration(tenantId, configId!),
|
||||
enabled: !!tenantId && !!configId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreatePOSConfiguration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreatePOSConfigurationRequest) =>
|
||||
posService.createConfiguration(tenantId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-configurations', tenantId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePOSConfiguration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ configId, data }: { configId: string; data: UpdatePOSConfigurationRequest }) =>
|
||||
posService.updateConfiguration(tenantId, configId, data),
|
||||
onSuccess: (_, { configId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-configurations', tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-configuration', tenantId, configId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePOSConfiguration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (configId: string) =>
|
||||
posService.deleteConfiguration(tenantId, configId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-configurations', tenantId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestPOSConnection = () => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (configId: string) =>
|
||||
posService.testConnection(tenantId, configId),
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SYNCHRONIZATION HOOKS
|
||||
// ============================================================================
|
||||
|
||||
export const useTriggerPOSSync = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ configId, syncRequest }: { configId: string; syncRequest: SyncRequest }) =>
|
||||
posService.triggerSync(tenantId, configId, syncRequest),
|
||||
onSuccess: (_, { configId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-sync-status', tenantId, configId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-sync-logs', tenantId, configId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const usePOSSyncStatus = (configId?: string, pollingInterval?: number) => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['pos-sync-status', tenantId, configId],
|
||||
queryFn: () => posService.getSyncStatus(tenantId, configId!),
|
||||
enabled: !!tenantId && !!configId,
|
||||
refetchInterval: pollingInterval || 30000, // Poll every 30 seconds by default
|
||||
staleTime: 10 * 1000, // 10 seconds
|
||||
});
|
||||
};
|
||||
|
||||
export const usePOSSyncLogs = (configId?: string, params?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: string;
|
||||
sync_type?: string;
|
||||
data_type?: string;
|
||||
}) => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['pos-sync-logs', tenantId, configId, params],
|
||||
queryFn: () => posService.getSyncLogs(tenantId, configId!, params),
|
||||
enabled: !!tenantId && !!configId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TRANSACTION HOOKS
|
||||
// ============================================================================
|
||||
|
||||
export const usePOSTransactions = (params?: {
|
||||
pos_system?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
status?: string;
|
||||
is_synced?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['pos-transactions', tenantId, params],
|
||||
queryFn: () => posService.getTransactions(tenantId, params),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
};
|
||||
|
||||
export const useSyncSingleTransaction = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ transactionId, force }: { transactionId: string; force?: boolean }) =>
|
||||
posService.syncSingleTransaction(tenantId, transactionId, force),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-transactions', tenantId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useResyncFailedTransactions = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (daysBack: number) =>
|
||||
posService.resyncFailedTransactions(tenantId, daysBack),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pos-transactions', tenantId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ANALYTICS HOOKS
|
||||
// ============================================================================
|
||||
|
||||
export const usePOSAnalytics = (days: number = 30) => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['pos-analytics', tenantId, days],
|
||||
queryFn: () => posService.getSyncAnalytics(tenantId, days),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM INFO HOOKS
|
||||
// ============================================================================
|
||||
|
||||
export const useSupportedPOSSystems = () => {
|
||||
return useQuery({
|
||||
queryKey: ['supported-pos-systems'],
|
||||
queryFn: () => posService.getSupportedSystems(),
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
});
|
||||
};
|
||||
|
||||
export const useWebhookStatus = (posSystem?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['webhook-status', posSystem],
|
||||
queryFn: () => posService.getWebhookStatus(posSystem!),
|
||||
enabled: !!posSystem,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// COMPOSITE HOOKS
|
||||
// ============================================================================
|
||||
|
||||
export const usePOSDashboard = () => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
// Get configurations
|
||||
const { data: configurationsData, isLoading: configurationsLoading } = usePOSConfigurations();
|
||||
|
||||
// Get recent transactions
|
||||
const { data: transactionsData, isLoading: transactionsLoading } = usePOSTransactions({
|
||||
limit: 10
|
||||
});
|
||||
|
||||
// Get analytics for last 7 days
|
||||
const { data: analyticsData, isLoading: analyticsLoading } = usePOSAnalytics(7);
|
||||
|
||||
const isLoading = configurationsLoading || transactionsLoading || analyticsLoading;
|
||||
|
||||
return {
|
||||
configurations: configurationsData?.configurations || [],
|
||||
transactions: transactionsData?.transactions || [],
|
||||
analytics: analyticsData,
|
||||
isLoading,
|
||||
summary: {
|
||||
total_configurations: configurationsData?.total || 0,
|
||||
active_configurations: configurationsData?.configurations?.filter(c => c.is_active).length || 0,
|
||||
connected_configurations: configurationsData?.configurations?.filter(c => c.is_connected).length || 0,
|
||||
total_transactions: transactionsData?.total || 0,
|
||||
total_revenue: transactionsData?.summary?.total_amount || 0,
|
||||
sync_health: analyticsData?.success_rate || 0,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const usePOSConfigurationManagement = () => {
|
||||
const createMutation = useCreatePOSConfiguration();
|
||||
const updateMutation = useUpdatePOSConfiguration();
|
||||
const deleteMutation = useDeletePOSConfiguration();
|
||||
const testConnectionMutation = useTestPOSConnection();
|
||||
|
||||
const [selectedConfiguration, setSelectedConfiguration] = useState<POSConfiguration | null>(null);
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
|
||||
const handleCreate = async (data: CreatePOSConfigurationRequest) => {
|
||||
await createMutation.mutateAsync(data);
|
||||
setIsFormOpen(false);
|
||||
};
|
||||
|
||||
const handleUpdate = async (configId: string, data: UpdatePOSConfigurationRequest) => {
|
||||
await updateMutation.mutateAsync({ configId, data });
|
||||
setIsFormOpen(false);
|
||||
setSelectedConfiguration(null);
|
||||
};
|
||||
|
||||
const handleDelete = async (configId: string) => {
|
||||
await deleteMutation.mutateAsync(configId);
|
||||
};
|
||||
|
||||
const handleTestConnection = async (configId: string) => {
|
||||
return await testConnectionMutation.mutateAsync(configId);
|
||||
};
|
||||
|
||||
const openCreateForm = () => {
|
||||
setSelectedConfiguration(null);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const openEditForm = (configuration: POSConfiguration) => {
|
||||
setSelectedConfiguration(configuration);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const closeForm = () => {
|
||||
setIsFormOpen(false);
|
||||
setSelectedConfiguration(null);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedConfiguration,
|
||||
isFormOpen,
|
||||
|
||||
// Actions
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
handleTestConnection,
|
||||
openCreateForm,
|
||||
openEditForm,
|
||||
closeForm,
|
||||
|
||||
// Loading states
|
||||
isCreating: createMutation.isPending,
|
||||
isUpdating: updateMutation.isPending,
|
||||
isDeleting: deleteMutation.isPending,
|
||||
isTesting: testConnectionMutation.isPending,
|
||||
|
||||
// Errors
|
||||
createError: createMutation.error,
|
||||
updateError: updateMutation.error,
|
||||
deleteError: deleteMutation.error,
|
||||
testError: testConnectionMutation.error,
|
||||
};
|
||||
};
|
||||
@@ -1,294 +0,0 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/hooks/useProcurement.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* React hooks for procurement planning functionality
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { procurementService } from '../services/procurement.service';
|
||||
import type {
|
||||
ProcurementPlan,
|
||||
GeneratePlanRequest,
|
||||
GeneratePlanResponse,
|
||||
DashboardData,
|
||||
ProcurementRequirement,
|
||||
PaginatedProcurementPlans
|
||||
} from '../types/procurement';
|
||||
|
||||
// ================================================================
|
||||
// QUERY KEYS
|
||||
// ================================================================
|
||||
|
||||
export const procurementKeys = {
|
||||
all: ['procurement'] as const,
|
||||
plans: () => [...procurementKeys.all, 'plans'] as const,
|
||||
plan: (id: string) => [...procurementKeys.plans(), id] as const,
|
||||
currentPlan: () => [...procurementKeys.plans(), 'current'] as const,
|
||||
planByDate: (date: string) => [...procurementKeys.plans(), 'date', date] as const,
|
||||
plansList: (filters?: any) => [...procurementKeys.plans(), 'list', filters] as const,
|
||||
requirements: () => [...procurementKeys.all, 'requirements'] as const,
|
||||
planRequirements: (planId: string) => [...procurementKeys.requirements(), 'plan', planId] as const,
|
||||
criticalRequirements: () => [...procurementKeys.requirements(), 'critical'] as const,
|
||||
dashboard: () => [...procurementKeys.all, 'dashboard'] as const,
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// PROCUREMENT PLAN HOOKS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch the current day's procurement plan
|
||||
*/
|
||||
export function useCurrentProcurementPlan() {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.currentPlan(),
|
||||
queryFn: () => procurementService.getCurrentPlan(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchInterval: 10 * 60 * 1000, // Refetch every 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch procurement plan by date
|
||||
*/
|
||||
export function useProcurementPlanByDate(date: string, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.planByDate(date),
|
||||
queryFn: () => procurementService.getPlanByDate(date),
|
||||
enabled: enabled && !!date,
|
||||
staleTime: 30 * 60 * 1000, // 30 minutes for historical data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch procurement plan by ID
|
||||
*/
|
||||
export function useProcurementPlan(planId: string, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.plan(planId),
|
||||
queryFn: () => procurementService.getPlanById(planId),
|
||||
enabled: enabled && !!planId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch paginated list of procurement plans
|
||||
*/
|
||||
export function useProcurementPlans(params?: {
|
||||
status?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.plansList(params),
|
||||
queryFn: () => procurementService.listPlans(params),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// REQUIREMENTS HOOKS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch requirements for a specific plan
|
||||
*/
|
||||
export function usePlanRequirements(
|
||||
planId: string,
|
||||
filters?: {
|
||||
status?: string;
|
||||
priority?: string;
|
||||
},
|
||||
enabled = true
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.planRequirements(planId),
|
||||
queryFn: () => procurementService.getPlanRequirements(planId, filters),
|
||||
enabled: enabled && !!planId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch critical requirements across all plans
|
||||
*/
|
||||
export function useCriticalRequirements() {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.criticalRequirements(),
|
||||
queryFn: () => procurementService.getCriticalRequirements(),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes for critical data
|
||||
refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// DASHBOARD HOOKS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch procurement dashboard data
|
||||
*/
|
||||
export function useProcurementDashboard() {
|
||||
return useQuery({
|
||||
queryKey: procurementKeys.dashboard(),
|
||||
queryFn: () => procurementService.getDashboardData(),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MUTATION HOOKS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Hook to generate a new procurement plan
|
||||
*/
|
||||
export function useGenerateProcurementPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (request: GeneratePlanRequest) =>
|
||||
procurementService.generatePlan(request),
|
||||
onSuccess: (data: GeneratePlanResponse) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: procurementKeys.plans() });
|
||||
queryClient.invalidateQueries({ queryKey: procurementKeys.dashboard() });
|
||||
|
||||
// If plan was generated successfully, update the cache
|
||||
if (data.success && data.plan) {
|
||||
queryClient.setQueryData(
|
||||
procurementKeys.plan(data.plan.id),
|
||||
data.plan
|
||||
);
|
||||
|
||||
// Update current plan cache if this is today's plan
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (data.plan.plan_date === today) {
|
||||
queryClient.setQueryData(
|
||||
procurementKeys.currentPlan(),
|
||||
data.plan
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to update procurement plan status
|
||||
*/
|
||||
export function useUpdatePlanStatus() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ planId, status }: { planId: string; status: string }) =>
|
||||
procurementService.updatePlanStatus(planId, status),
|
||||
onSuccess: (updatedPlan: ProcurementPlan) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(
|
||||
procurementKeys.plan(updatedPlan.id),
|
||||
updatedPlan
|
||||
);
|
||||
|
||||
// Update current plan if this is the current plan
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (updatedPlan.plan_date === today) {
|
||||
queryClient.setQueryData(
|
||||
procurementKeys.currentPlan(),
|
||||
updatedPlan
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate lists to ensure they're refreshed
|
||||
queryClient.invalidateQueries({ queryKey: procurementKeys.plansList() });
|
||||
queryClient.invalidateQueries({ queryKey: procurementKeys.dashboard() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to trigger the daily scheduler manually
|
||||
*/
|
||||
export function useTriggerDailyScheduler() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => procurementService.triggerDailyScheduler(),
|
||||
onSuccess: () => {
|
||||
// Invalidate all procurement data
|
||||
queryClient.invalidateQueries({ queryKey: procurementKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// UTILITY HOOKS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Hook to check procurement service health
|
||||
*/
|
||||
export function useProcurementHealth() {
|
||||
return useQuery({
|
||||
queryKey: [...procurementKeys.all, 'health'],
|
||||
queryFn: () => procurementService.healthCheck(),
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
refetchInterval: 5 * 60 * 1000, // Check every 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// COMBINED HOOKS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Combined hook for procurement plan dashboard
|
||||
* Fetches current plan, dashboard data, and critical requirements
|
||||
*/
|
||||
export function useProcurementPlanDashboard() {
|
||||
const currentPlan = useCurrentProcurementPlan();
|
||||
const dashboard = useProcurementDashboard();
|
||||
const criticalRequirements = useCriticalRequirements();
|
||||
const health = useProcurementHealth();
|
||||
|
||||
return {
|
||||
currentPlan,
|
||||
dashboard,
|
||||
criticalRequirements,
|
||||
health,
|
||||
isLoading: currentPlan.isLoading || dashboard.isLoading,
|
||||
error: currentPlan.error || dashboard.error || criticalRequirements.error,
|
||||
refetchAll: () => {
|
||||
currentPlan.refetch();
|
||||
dashboard.refetch();
|
||||
criticalRequirements.refetch();
|
||||
health.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing procurement plan lifecycle
|
||||
*/
|
||||
export function useProcurementPlanActions() {
|
||||
const generatePlan = useGenerateProcurementPlan();
|
||||
const updateStatus = useUpdatePlanStatus();
|
||||
const triggerScheduler = useTriggerDailyScheduler();
|
||||
|
||||
return {
|
||||
generatePlan: generatePlan.mutate,
|
||||
updateStatus: updateStatus.mutate,
|
||||
triggerScheduler: triggerScheduler.mutate,
|
||||
isGenerating: generatePlan.isPending,
|
||||
isUpdating: updateStatus.isPending,
|
||||
isTriggering: triggerScheduler.isPending,
|
||||
generateError: generatePlan.error,
|
||||
updateError: updateStatus.error,
|
||||
triggerError: triggerScheduler.error,
|
||||
};
|
||||
}
|
||||
@@ -1,682 +0,0 @@
|
||||
// frontend/src/api/hooks/useRecipes.ts
|
||||
/**
|
||||
* React hooks for recipe and production management
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import {
|
||||
RecipesService,
|
||||
Recipe,
|
||||
RecipeIngredient,
|
||||
CreateRecipeRequest,
|
||||
UpdateRecipeRequest,
|
||||
RecipeSearchParams,
|
||||
RecipeFeasibility,
|
||||
RecipeStatistics,
|
||||
ProductionBatch,
|
||||
CreateProductionBatchRequest,
|
||||
UpdateProductionBatchRequest,
|
||||
ProductionBatchSearchParams,
|
||||
ProductionStatistics
|
||||
} from '../services/recipes.service';
|
||||
import { useTenant } from './useTenant';
|
||||
import { useAuth } from './useAuth';
|
||||
|
||||
const recipesService = new RecipesService();
|
||||
|
||||
// Recipe Management Hook
|
||||
export interface UseRecipesReturn {
|
||||
// Data
|
||||
recipes: Recipe[];
|
||||
selectedRecipe: Recipe | null;
|
||||
categories: string[];
|
||||
statistics: RecipeStatistics | null;
|
||||
|
||||
// State
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
// Actions
|
||||
loadRecipes: (params?: RecipeSearchParams) => Promise<void>;
|
||||
loadRecipe: (recipeId: string) => Promise<void>;
|
||||
createRecipe: (data: CreateRecipeRequest) => Promise<Recipe | null>;
|
||||
updateRecipe: (recipeId: string, data: UpdateRecipeRequest) => Promise<Recipe | null>;
|
||||
deleteRecipe: (recipeId: string) => Promise<boolean>;
|
||||
duplicateRecipe: (recipeId: string, newName: string) => Promise<Recipe | null>;
|
||||
activateRecipe: (recipeId: string) => Promise<Recipe | null>;
|
||||
checkFeasibility: (recipeId: string, batchMultiplier?: number) => Promise<RecipeFeasibility | null>;
|
||||
loadStatistics: () => Promise<void>;
|
||||
loadCategories: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
setPage: (page: number) => void;
|
||||
}
|
||||
|
||||
export const useRecipes = (autoLoad: boolean = true): UseRecipesReturn => {
|
||||
const { currentTenant } = useTenant();
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [statistics, setStatistics] = useState<RecipeStatistics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentParams, setCurrentParams] = useState<RecipeSearchParams>({});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Load recipes
|
||||
const loadRecipes = useCallback(async (params: RecipeSearchParams = {}) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const searchParams = {
|
||||
...params,
|
||||
limit: pagination.limit,
|
||||
offset: (pagination.page - 1) * pagination.limit
|
||||
};
|
||||
|
||||
const recipesData = await recipesService.getRecipes(currentTenant.id, searchParams);
|
||||
setRecipes(recipesData);
|
||||
setCurrentParams(params);
|
||||
|
||||
// Calculate pagination (assuming we get total count somehow)
|
||||
const total = recipesData.length; // This would need to be from a proper paginated response
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total,
|
||||
totalPages: Math.ceil(total / prev.limit)
|
||||
}));
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading recipes';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id, pagination.page, pagination.limit]);
|
||||
|
||||
// Load single recipe
|
||||
const loadRecipe = useCallback(async (recipeId: string) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const recipe = await recipesService.getRecipe(currentTenant.id, recipeId);
|
||||
setSelectedRecipe(recipe);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Create recipe
|
||||
const createRecipe = useCallback(async (data: CreateRecipeRequest): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const newRecipe = await recipesService.createRecipe(currentTenant.id, user.id, data);
|
||||
|
||||
// Add to local state
|
||||
setRecipes(prev => [newRecipe, ...prev]);
|
||||
|
||||
toast.success('Recipe created successfully');
|
||||
return newRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error creating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id]);
|
||||
|
||||
// Update recipe
|
||||
const updateRecipe = useCallback(async (recipeId: string, data: UpdateRecipeRequest): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updatedRecipe = await recipesService.updateRecipe(currentTenant.id, user.id, recipeId, data);
|
||||
|
||||
// Update local state
|
||||
setRecipes(prev => prev.map(recipe =>
|
||||
recipe.id === recipeId ? updatedRecipe : recipe
|
||||
));
|
||||
|
||||
if (selectedRecipe?.id === recipeId) {
|
||||
setSelectedRecipe(updatedRecipe);
|
||||
}
|
||||
|
||||
toast.success('Recipe updated successfully');
|
||||
return updatedRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedRecipe?.id]);
|
||||
|
||||
// Delete recipe
|
||||
const deleteRecipe = useCallback(async (recipeId: string): Promise<boolean> => {
|
||||
if (!currentTenant?.id) return false;
|
||||
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await recipesService.deleteRecipe(currentTenant.id, recipeId);
|
||||
|
||||
// Remove from local state
|
||||
setRecipes(prev => prev.filter(recipe => recipe.id !== recipeId));
|
||||
|
||||
if (selectedRecipe?.id === recipeId) {
|
||||
setSelectedRecipe(null);
|
||||
}
|
||||
|
||||
toast.success('Recipe deleted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error deleting recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [currentTenant?.id, selectedRecipe?.id]);
|
||||
|
||||
// Duplicate recipe
|
||||
const duplicateRecipe = useCallback(async (recipeId: string, newName: string): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const duplicatedRecipe = await recipesService.duplicateRecipe(currentTenant.id, user.id, recipeId, newName);
|
||||
|
||||
// Add to local state
|
||||
setRecipes(prev => [duplicatedRecipe, ...prev]);
|
||||
|
||||
toast.success('Recipe duplicated successfully');
|
||||
return duplicatedRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error duplicating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id]);
|
||||
|
||||
// Activate recipe
|
||||
const activateRecipe = useCallback(async (recipeId: string): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const activatedRecipe = await recipesService.activateRecipe(currentTenant.id, user.id, recipeId);
|
||||
|
||||
// Update local state
|
||||
setRecipes(prev => prev.map(recipe =>
|
||||
recipe.id === recipeId ? activatedRecipe : recipe
|
||||
));
|
||||
|
||||
if (selectedRecipe?.id === recipeId) {
|
||||
setSelectedRecipe(activatedRecipe);
|
||||
}
|
||||
|
||||
toast.success('Recipe activated successfully');
|
||||
return activatedRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error activating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedRecipe?.id]);
|
||||
|
||||
// Check feasibility
|
||||
const checkFeasibility = useCallback(async (recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibility | null> => {
|
||||
if (!currentTenant?.id) return null;
|
||||
|
||||
try {
|
||||
const feasibility = await recipesService.checkRecipeFeasibility(currentTenant.id, recipeId, batchMultiplier);
|
||||
return feasibility;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error checking recipe feasibility';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load statistics
|
||||
const loadStatistics = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const stats = await recipesService.getRecipeStatistics(currentTenant.id);
|
||||
setStatistics(stats);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading recipe statistics:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load categories
|
||||
const loadCategories = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const cats = await recipesService.getRecipeCategories(currentTenant.id);
|
||||
setCategories(cats);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading recipe categories:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Refresh
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([
|
||||
loadRecipes(currentParams),
|
||||
loadStatistics(),
|
||||
loadCategories()
|
||||
]);
|
||||
}, [loadRecipes, currentParams, loadStatistics, loadCategories]);
|
||||
|
||||
// Set page
|
||||
const setPage = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
}, []);
|
||||
|
||||
// Auto-load on mount and dependencies change
|
||||
useEffect(() => {
|
||||
if (autoLoad && currentTenant?.id) {
|
||||
refresh();
|
||||
}
|
||||
}, [autoLoad, currentTenant?.id, pagination.page]);
|
||||
|
||||
return {
|
||||
// Data
|
||||
recipes,
|
||||
selectedRecipe,
|
||||
categories,
|
||||
statistics,
|
||||
|
||||
// State
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
error,
|
||||
pagination,
|
||||
|
||||
// Actions
|
||||
loadRecipes,
|
||||
loadRecipe,
|
||||
createRecipe,
|
||||
updateRecipe,
|
||||
deleteRecipe,
|
||||
duplicateRecipe,
|
||||
activateRecipe,
|
||||
checkFeasibility,
|
||||
loadStatistics,
|
||||
loadCategories,
|
||||
clearError,
|
||||
refresh,
|
||||
setPage
|
||||
};
|
||||
};
|
||||
|
||||
// Production Management Hook
|
||||
export interface UseProductionReturn {
|
||||
// Data
|
||||
batches: ProductionBatch[];
|
||||
selectedBatch: ProductionBatch | null;
|
||||
activeBatches: ProductionBatch[];
|
||||
statistics: ProductionStatistics | null;
|
||||
|
||||
// State
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
loadBatches: (params?: ProductionBatchSearchParams) => Promise<void>;
|
||||
loadBatch: (batchId: string) => Promise<void>;
|
||||
loadActiveBatches: () => Promise<void>;
|
||||
createBatch: (data: CreateProductionBatchRequest) => Promise<ProductionBatch | null>;
|
||||
updateBatch: (batchId: string, data: UpdateProductionBatchRequest) => Promise<ProductionBatch | null>;
|
||||
deleteBatch: (batchId: string) => Promise<boolean>;
|
||||
startBatch: (batchId: string, data: any) => Promise<ProductionBatch | null>;
|
||||
completeBatch: (batchId: string, data: any) => Promise<ProductionBatch | null>;
|
||||
loadStatistics: (startDate?: string, endDate?: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useProduction = (autoLoad: boolean = true): UseProductionReturn => {
|
||||
const { currentTenant } = useTenant();
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [batches, setBatches] = useState<ProductionBatch[]>([]);
|
||||
const [selectedBatch, setSelectedBatch] = useState<ProductionBatch | null>(null);
|
||||
const [activeBatches, setActiveBatches] = useState<ProductionBatch[]>([]);
|
||||
const [statistics, setStatistics] = useState<ProductionStatistics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load batches
|
||||
const loadBatches = useCallback(async (params: ProductionBatchSearchParams = {}) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const batchesData = await recipesService.getProductionBatches(currentTenant.id, params);
|
||||
setBatches(batchesData);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading production batches';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load single batch
|
||||
const loadBatch = useCallback(async (batchId: string) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const batch = await recipesService.getProductionBatch(currentTenant.id, batchId);
|
||||
setSelectedBatch(batch);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load active batches
|
||||
const loadActiveBatches = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const activeBatchesData = await recipesService.getActiveProductionBatches(currentTenant.id);
|
||||
setActiveBatches(activeBatchesData);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading active batches:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Create batch
|
||||
const createBatch = useCallback(async (data: CreateProductionBatchRequest): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const newBatch = await recipesService.createProductionBatch(currentTenant.id, user.id, data);
|
||||
|
||||
// Add to local state
|
||||
setBatches(prev => [newBatch, ...prev]);
|
||||
|
||||
toast.success('Production batch created successfully');
|
||||
return newBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error creating production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id]);
|
||||
|
||||
// Update batch
|
||||
const updateBatch = useCallback(async (batchId: string, data: UpdateProductionBatchRequest): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updatedBatch = await recipesService.updateProductionBatch(currentTenant.id, user.id, batchId, data);
|
||||
|
||||
// Update local state
|
||||
setBatches(prev => prev.map(batch =>
|
||||
batch.id === batchId ? updatedBatch : batch
|
||||
));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(updatedBatch);
|
||||
}
|
||||
|
||||
toast.success('Production batch updated successfully');
|
||||
return updatedBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedBatch?.id]);
|
||||
|
||||
// Delete batch
|
||||
const deleteBatch = useCallback(async (batchId: string): Promise<boolean> => {
|
||||
if (!currentTenant?.id) return false;
|
||||
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await recipesService.deleteProductionBatch(currentTenant.id, batchId);
|
||||
|
||||
// Remove from local state
|
||||
setBatches(prev => prev.filter(batch => batch.id !== batchId));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(null);
|
||||
}
|
||||
|
||||
toast.success('Production batch deleted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error deleting production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [currentTenant?.id, selectedBatch?.id]);
|
||||
|
||||
// Start batch
|
||||
const startBatch = useCallback(async (batchId: string, data: any): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const startedBatch = await recipesService.startProductionBatch(currentTenant.id, user.id, batchId, data);
|
||||
|
||||
// Update local state
|
||||
setBatches(prev => prev.map(batch =>
|
||||
batch.id === batchId ? startedBatch : batch
|
||||
));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(startedBatch);
|
||||
}
|
||||
|
||||
toast.success('Production batch started successfully');
|
||||
return startedBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error starting production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedBatch?.id]);
|
||||
|
||||
// Complete batch
|
||||
const completeBatch = useCallback(async (batchId: string, data: any): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const completedBatch = await recipesService.completeProductionBatch(currentTenant.id, user.id, batchId, data);
|
||||
|
||||
// Update local state
|
||||
setBatches(prev => prev.map(batch =>
|
||||
batch.id === batchId ? completedBatch : batch
|
||||
));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(completedBatch);
|
||||
}
|
||||
|
||||
toast.success('Production batch completed successfully');
|
||||
return completedBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error completing production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedBatch?.id]);
|
||||
|
||||
// Load statistics
|
||||
const loadStatistics = useCallback(async (startDate?: string, endDate?: string) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const stats = await recipesService.getProductionStatistics(currentTenant.id, startDate, endDate);
|
||||
setStatistics(stats);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading production statistics:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Refresh
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([
|
||||
loadBatches(),
|
||||
loadActiveBatches(),
|
||||
loadStatistics()
|
||||
]);
|
||||
}, [loadBatches, loadActiveBatches, loadStatistics]);
|
||||
|
||||
// Auto-load on mount
|
||||
useEffect(() => {
|
||||
if (autoLoad && currentTenant?.id) {
|
||||
refresh();
|
||||
}
|
||||
}, [autoLoad, currentTenant?.id]);
|
||||
|
||||
return {
|
||||
// Data
|
||||
batches,
|
||||
selectedBatch,
|
||||
activeBatches,
|
||||
statistics,
|
||||
|
||||
// State
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
loadBatches,
|
||||
loadBatch,
|
||||
loadActiveBatches,
|
||||
createBatch,
|
||||
updateBatch,
|
||||
deleteBatch,
|
||||
startBatch,
|
||||
completeBatch,
|
||||
loadStatistics,
|
||||
clearError,
|
||||
refresh
|
||||
};
|
||||
};
|
||||
@@ -1,200 +0,0 @@
|
||||
// frontend/src/api/hooks/useSales.ts
|
||||
/**
|
||||
* Sales Data Management Hooks
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { salesService } from '../services/sales.service';
|
||||
import type {
|
||||
SalesData,
|
||||
SalesValidationResult,
|
||||
SalesDataQuery,
|
||||
SalesDataImport,
|
||||
SalesImportResult,
|
||||
DashboardStats,
|
||||
ActivityItem,
|
||||
} from '../types';
|
||||
|
||||
export const useSales = () => {
|
||||
const [salesData, setSalesData] = useState<SalesData[]>([]);
|
||||
const [dashboardStats, setDashboardStats] = useState<DashboardStats | null>(null);
|
||||
const [recentActivity, setRecentActivity] = useState<ActivityItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||
|
||||
const uploadSalesHistory = useCallback(async (
|
||||
tenantId: string,
|
||||
file: File,
|
||||
additionalData?: Record<string, any>
|
||||
): Promise<SalesImportResult> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
const result = await salesService.uploadSalesHistory(tenantId, file, {
|
||||
...additionalData,
|
||||
onProgress: (progress) => {
|
||||
setUploadProgress(progress.percentage);
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Upload failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const validateSalesData = useCallback(async (
|
||||
tenantId: string,
|
||||
file: File
|
||||
): Promise<SalesValidationResult> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await salesService.validateSalesData(tenantId, file);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Validation failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getSalesData = useCallback(async (
|
||||
tenantId: string,
|
||||
query?: SalesDataQuery
|
||||
): Promise<SalesData[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await salesService.getSalesData(tenantId, query);
|
||||
setSalesData(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get sales data';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getDashboardStats = useCallback(async (tenantId: string): Promise<DashboardStats> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const stats = await salesService.getDashboardStats(tenantId);
|
||||
setDashboardStats(stats);
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get dashboard stats';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getRecentActivity = useCallback(async (tenantId: string, limit?: number): Promise<ActivityItem[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const activity = await salesService.getRecentActivity(tenantId, limit);
|
||||
setRecentActivity(activity);
|
||||
|
||||
return activity;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get recent activity';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const exportSalesData = useCallback(async (
|
||||
tenantId: string,
|
||||
format: 'csv' | 'excel' | 'json',
|
||||
query?: SalesDataQuery
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const blob = await salesService.exportSalesData(tenantId, format, query);
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `sales-data.${format}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Export failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
/**
|
||||
* Get Sales Analytics
|
||||
*/
|
||||
const getSalesAnalytics = useCallback(async (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const analytics = await salesService.getSalesAnalytics(tenantId, startDate, endDate);
|
||||
|
||||
return analytics;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get sales analytics';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
salesData,
|
||||
dashboardStats,
|
||||
recentActivity,
|
||||
isLoading,
|
||||
error,
|
||||
uploadProgress,
|
||||
uploadSalesHistory,
|
||||
validateSalesData,
|
||||
getSalesData,
|
||||
getDashboardStats,
|
||||
getRecentActivity,
|
||||
exportSalesData,
|
||||
getSalesAnalytics,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
// Simplified useSuppliers hook for TypeScript compatibility
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
SupplierSummary,
|
||||
CreateSupplierRequest,
|
||||
UpdateSupplierRequest,
|
||||
SupplierSearchParams,
|
||||
SupplierStatistics,
|
||||
PurchaseOrder,
|
||||
CreatePurchaseOrderRequest,
|
||||
PurchaseOrderSearchParams,
|
||||
PurchaseOrderStatistics,
|
||||
Delivery,
|
||||
DeliverySearchParams,
|
||||
DeliveryPerformanceStats
|
||||
} from '../services/suppliers.service';
|
||||
|
||||
export const useSuppliers = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Simple stub implementations
|
||||
const getSuppliers = async (params?: SupplierSearchParams) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Mock data for now
|
||||
return [];
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createSupplier = async (data: CreateSupplierRequest) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Mock implementation
|
||||
return { id: '1', ...data } as any;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSupplier = async (id: string, data: UpdateSupplierRequest) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Mock implementation
|
||||
return { id, ...data } as any;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Return all the expected properties/methods
|
||||
return {
|
||||
suppliers: [],
|
||||
isLoading,
|
||||
error,
|
||||
getSuppliers,
|
||||
createSupplier,
|
||||
updateSupplier,
|
||||
deleteSupplier: async () => {},
|
||||
getSupplierStatistics: async () => ({} as SupplierStatistics),
|
||||
getActiveSuppliers: async () => [] as SupplierSummary[],
|
||||
getTopSuppliers: async () => [] as SupplierSummary[],
|
||||
getSuppliersNeedingReview: async () => [] as SupplierSummary[],
|
||||
approveSupplier: async () => {},
|
||||
// Purchase orders
|
||||
getPurchaseOrders: async () => [] as PurchaseOrder[],
|
||||
createPurchaseOrder: async () => ({} as PurchaseOrder),
|
||||
updatePurchaseOrderStatus: async () => ({} as PurchaseOrder),
|
||||
// Deliveries
|
||||
getDeliveries: async () => [] as Delivery[],
|
||||
getTodaysDeliveries: async () => [] as Delivery[],
|
||||
getDeliveryPerformanceStats: async () => ({} as DeliveryPerformanceStats),
|
||||
};
|
||||
};
|
||||
|
||||
// Re-export types
|
||||
export type {
|
||||
SupplierSummary,
|
||||
CreateSupplierRequest,
|
||||
UpdateSupplierRequest,
|
||||
SupplierSearchParams,
|
||||
SupplierStatistics,
|
||||
PurchaseOrder,
|
||||
CreatePurchaseOrderRequest,
|
||||
PurchaseOrderSearchParams,
|
||||
PurchaseOrderStatistics,
|
||||
Delivery,
|
||||
DeliverySearchParams,
|
||||
DeliveryPerformanceStats
|
||||
};
|
||||
@@ -1,209 +0,0 @@
|
||||
// frontend/src/api/hooks/useTenant.ts
|
||||
/**
|
||||
* Tenant Management Hooks
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { tenantService } from '../services';
|
||||
import type {
|
||||
TenantInfo,
|
||||
TenantCreate,
|
||||
TenantUpdate,
|
||||
TenantMember,
|
||||
InviteUser,
|
||||
TenantStats,
|
||||
} from '../types';
|
||||
|
||||
export const useTenant = () => {
|
||||
const [tenants, setTenants] = useState<TenantInfo[]>([]);
|
||||
const [currentTenant, setCurrentTenant] = useState<TenantInfo | null>(null);
|
||||
const [members, setMembers] = useState<TenantMember[]>([]);
|
||||
const [stats, setStats] = useState<TenantStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const createTenant = useCallback(async (data: TenantCreate): Promise<TenantInfo> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const tenant = await tenantService.createTenant(data);
|
||||
setTenants(prev => [...prev, tenant]);
|
||||
setCurrentTenant(tenant);
|
||||
|
||||
return tenant;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create tenant';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTenant = useCallback(async (tenantId: string): Promise<TenantInfo> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const tenant = await tenantService.getTenant(tenantId);
|
||||
setCurrentTenant(tenant);
|
||||
|
||||
return tenant;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get tenant';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateTenant = useCallback(async (tenantId: string, data: TenantUpdate): Promise<TenantInfo> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const updatedTenant = await tenantService.updateTenant(tenantId, data);
|
||||
setCurrentTenant(updatedTenant);
|
||||
setTenants(prev => prev.map(t => t.id === tenantId ? updatedTenant : t));
|
||||
|
||||
return updatedTenant;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update tenant';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getUserTenants = useCallback(async (): Promise<TenantInfo[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const userTenants = await tenantService.getUserTenants();
|
||||
setTenants(userTenants);
|
||||
|
||||
return userTenants;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get user tenants';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTenantMembers = useCallback(async (tenantId: string): Promise<TenantMember[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await tenantService.getTenantMembers(tenantId);
|
||||
setMembers(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get tenant members';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const inviteUser = useCallback(async (tenantId: string, invitation: InviteUser): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await tenantService.inviteUser(tenantId, invitation);
|
||||
|
||||
// Refresh members list
|
||||
await getTenantMembers(tenantId);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to invite user';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [getTenantMembers]);
|
||||
|
||||
const removeMember = useCallback(async (tenantId: string, userId: string): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await tenantService.removeMember(tenantId, userId);
|
||||
setMembers(prev => prev.filter(m => m.user_id !== userId));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to remove member';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateMemberRole = useCallback(async (tenantId: string, userId: string, role: string): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const updatedMember = await tenantService.updateMemberRole(tenantId, userId, role);
|
||||
setMembers(prev => prev.map(m => m.user_id === userId ? updatedMember : m));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update member role';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTenantStats = useCallback(async (tenantId: string): Promise<TenantStats> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const tenantStats = await tenantService.getTenantStats(tenantId);
|
||||
setStats(tenantStats);
|
||||
|
||||
return tenantStats;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get tenant stats';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
tenants,
|
||||
currentTenant,
|
||||
members,
|
||||
stats,
|
||||
isLoading,
|
||||
error,
|
||||
createTenant,
|
||||
getTenant,
|
||||
updateTenant,
|
||||
getUserTenants,
|
||||
getTenantMembers,
|
||||
inviteUser,
|
||||
removeMember,
|
||||
updateMemberRole,
|
||||
getTenantStats,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
|
||||
// Hook to get current tenant ID from context or state
|
||||
export const useTenantId = () => {
|
||||
const { currentTenant } = useTenant();
|
||||
return currentTenant?.id || null;
|
||||
};
|
||||
@@ -1,265 +0,0 @@
|
||||
// frontend/src/api/hooks/useTraining.ts
|
||||
/**
|
||||
* Training Operations Hooks
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { trainingService } from '../services';
|
||||
import type {
|
||||
TrainingJobRequest,
|
||||
TrainingJobResponse,
|
||||
ModelInfo,
|
||||
ModelTrainingStats,
|
||||
SingleProductTrainingRequest,
|
||||
} from '../types';
|
||||
|
||||
interface UseTrainingOptions {
|
||||
disablePolling?: boolean; // New option to disable HTTP status polling
|
||||
}
|
||||
|
||||
export const useTraining = (options: UseTrainingOptions = {}) => {
|
||||
|
||||
const { disablePolling = false } = options;
|
||||
|
||||
// Debug logging for option changes
|
||||
console.log('🔧 useTraining initialized with options:', { disablePolling, options });
|
||||
const [jobs, setJobs] = useState<TrainingJobResponse[]>([]);
|
||||
const [currentJob, setCurrentJob] = useState<TrainingJobResponse | null>(null);
|
||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||
const [stats, setStats] = useState<ModelTrainingStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const startTrainingJob = useCallback(async (
|
||||
tenantId: string,
|
||||
request: TrainingJobRequest
|
||||
): Promise<TrainingJobResponse> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const job = await trainingService.startTrainingJob(tenantId, request);
|
||||
setCurrentJob(job);
|
||||
setJobs(prev => [job, ...prev]);
|
||||
|
||||
return job;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to start training job';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startSingleProductTraining = useCallback(async (
|
||||
tenantId: string,
|
||||
request: SingleProductTrainingRequest
|
||||
): Promise<TrainingJobResponse> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const job = await trainingService.startSingleProductTraining(tenantId, request);
|
||||
setCurrentJob(job);
|
||||
setJobs(prev => [job, ...prev]);
|
||||
|
||||
return job;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to start product training';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTrainingJobStatus = useCallback(async (
|
||||
tenantId: string,
|
||||
jobId: string
|
||||
): Promise<TrainingJobResponse> => {
|
||||
try {
|
||||
const job = await trainingService.getTrainingJobStatus(tenantId, jobId);
|
||||
|
||||
// Update job in state
|
||||
setJobs(prev => prev.map(j => j.job_id === jobId ? job : j));
|
||||
if (currentJob?.job_id === jobId) {
|
||||
setCurrentJob(job);
|
||||
}
|
||||
|
||||
return job;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get job status';
|
||||
setError(message);
|
||||
throw error;
|
||||
}
|
||||
}, [currentJob]);
|
||||
|
||||
const cancelTrainingJob = useCallback(async (
|
||||
tenantId: string,
|
||||
jobId: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await trainingService.cancelTrainingJob(tenantId, jobId);
|
||||
|
||||
// Update job status in state
|
||||
setJobs(prev => prev.map(j =>
|
||||
j.job_id === jobId ? { ...j, status: 'cancelled' } : j
|
||||
));
|
||||
if (currentJob?.job_id === jobId) {
|
||||
setCurrentJob({ ...currentJob, status: 'cancelled' });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to cancel job';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentJob]);
|
||||
|
||||
const getTrainingJobs = useCallback(async (tenantId: string): Promise<TrainingJobResponse[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await trainingService.getTrainingJobs(tenantId);
|
||||
setJobs(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get training jobs';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getModels = useCallback(async (tenantId: string): Promise<ModelInfo[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await trainingService.getModels(tenantId);
|
||||
setModels(response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get models';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const validateTrainingData = useCallback(async (tenantId: string): Promise<{
|
||||
is_valid: boolean;
|
||||
message: string;
|
||||
details?: any;
|
||||
}> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await trainingService.validateTrainingData(tenantId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Data validation failed';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTrainingStats = useCallback(async (tenantId: string): Promise<ModelTrainingStats> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const trainingStats = await trainingService.getTrainingStats(tenantId);
|
||||
setStats(trainingStats);
|
||||
|
||||
return trainingStats;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get training stats';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Always check disablePolling first and log for debugging
|
||||
console.log('🔍 useTraining polling check:', {
|
||||
disablePolling,
|
||||
jobsCount: jobs.length,
|
||||
runningJobs: jobs.filter(job => job.status === 'running' || job.status === 'pending').length
|
||||
});
|
||||
|
||||
// STRICT CHECK: Skip polling if disabled - NO EXCEPTIONS
|
||||
if (disablePolling === true) {
|
||||
console.log('🚫 HTTP status polling STRICTLY DISABLED - using WebSocket instead');
|
||||
console.log('🚫 Effect triggered but polling prevented by disablePolling flag');
|
||||
return; // Early return - no cleanup needed, no interval creation
|
||||
}
|
||||
|
||||
const runningJobs = jobs.filter(job => job.status === 'running' || job.status === 'pending');
|
||||
|
||||
if (runningJobs.length === 0) {
|
||||
console.log('⏸️ No running jobs - skipping polling setup');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔄 Starting HTTP status polling for', runningJobs.length, 'jobs');
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
// Double-check disablePolling inside interval to prevent race conditions
|
||||
if (disablePolling) {
|
||||
console.log('🚫 Polling disabled during interval - clearing');
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const job of runningJobs) {
|
||||
try {
|
||||
const tenantId = job.tenant_id;
|
||||
console.log('📡 HTTP polling job status:', job.job_id);
|
||||
await getTrainingJobStatus(tenantId, job.job_id);
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh job status:', error);
|
||||
}
|
||||
}
|
||||
}, 5000); // Refresh every 5 seconds
|
||||
|
||||
return () => {
|
||||
console.log('🛑 Stopping HTTP status polling (cleanup)');
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [jobs, getTrainingJobStatus, disablePolling]);
|
||||
|
||||
|
||||
return {
|
||||
jobs,
|
||||
currentJob,
|
||||
models,
|
||||
stats,
|
||||
isLoading,
|
||||
error,
|
||||
startTrainingJob,
|
||||
startSingleProductTraining,
|
||||
getTrainingJobStatus,
|
||||
cancelTrainingJob,
|
||||
getTrainingJobs,
|
||||
getModels,
|
||||
validateTrainingData,
|
||||
getTrainingStats,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user