ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -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
};
};

View File

@@ -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 };
};

View File

@@ -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),
};
};

View File

@@ -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),
};
};

View File

@@ -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,
};
};

View File

@@ -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),
};
};

View File

@@ -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,
};
};

View File

@@ -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,
};
};

View File

@@ -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,
};
}

View File

@@ -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
};
};

View File

@@ -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),
};
};

View File

@@ -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
};

View File

@@ -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;
};

View File

@@ -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),
};
};