Fix new services implementation 5
This commit is contained in:
@@ -16,13 +16,28 @@ class AuthInterceptor {
|
||||
static setup() {
|
||||
apiClient.addRequestInterceptor({
|
||||
onRequest: async (config: RequestConfig) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
let token = localStorage.getItem('auth_token');
|
||||
|
||||
console.log('🔐 AuthInterceptor: Checking auth token...', token ? 'Found' : 'Missing');
|
||||
if (token) {
|
||||
console.log('🔐 AuthInterceptor: Token preview:', token.substring(0, 20) + '...');
|
||||
}
|
||||
|
||||
// For development: If no token exists or token is invalid, set a valid demo token
|
||||
if ((!token || token === 'demo-development-token') && window.location.hostname === 'localhost') {
|
||||
console.log('🔧 AuthInterceptor: Development mode - setting valid demo token');
|
||||
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxN2Q1ZTJjZC1hMjk4LTQyNzEtODZjNi01NmEzZGNiNDE0ZWUiLCJ1c2VyX2lkIjoiMTdkNWUyY2QtYTI5OC00MjcxLTg2YzYtNTZhM2RjYjQxNGVlIiwiZW1haWwiOiJ0ZXN0QGRlbW8uY29tIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NTI3MzEyNSwiaWF0IjoxNzU1MjcxMzI1LCJpc3MiOiJiYWtlcnktYXV0aCIsImZ1bGxfbmFtZSI6IkRlbW8gVXNlciIsImlzX3ZlcmlmaWVkIjpmYWxzZSwiaXNfYWN0aXZlIjp0cnVlLCJyb2xlIjoidXNlciJ9.RBfzH9L_NKySYkyLzBLYAApnrCFNK4OsGLLO-eCaTSI';
|
||||
localStorage.setItem('auth_token', token);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
console.log('🔐 AuthInterceptor: Added Authorization header');
|
||||
} else {
|
||||
console.warn('⚠️ AuthInterceptor: No auth token found in localStorage');
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -11,7 +11,7 @@ export { useTraining } from './useTraining';
|
||||
export { useForecast } from './useForecast';
|
||||
export { useNotification } from './useNotification';
|
||||
export { useOnboarding, useOnboardingStep } from './useOnboarding';
|
||||
export { useInventory, useInventoryDashboard, useInventoryItem } from './useInventory';
|
||||
export { useInventory, useInventoryDashboard, useInventoryItem, useInventoryProducts } from './useInventory';
|
||||
export { useRecipes, useProduction } from './useRecipes';
|
||||
|
||||
// Import hooks for combined usage
|
||||
@@ -23,6 +23,7 @@ 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 = () => {
|
||||
@@ -30,10 +31,11 @@ export const useApiHooks = () => {
|
||||
const tenant = useTenant();
|
||||
const sales = useSales();
|
||||
const external = useExternal();
|
||||
const training = useTraining();
|
||||
const training = useTraining({ disablePolling: true }); // Disable polling by default
|
||||
const forecast = useForecast();
|
||||
const notification = useNotification();
|
||||
const onboarding = useOnboarding();
|
||||
const inventory = useInventory();
|
||||
|
||||
return {
|
||||
auth,
|
||||
@@ -44,5 +46,6 @@ export const useApiHooks = () => {
|
||||
forecast,
|
||||
notification,
|
||||
onboarding,
|
||||
inventory
|
||||
};
|
||||
};
|
||||
@@ -31,7 +31,7 @@ export const useForecast = () => {
|
||||
setError(null);
|
||||
|
||||
const newForecasts = await forecastingService.createSingleForecast(tenantId, request);
|
||||
setForecasts(prev => [...newForecasts, ...prev]);
|
||||
setForecasts(prev => [...newForecasts, ...(prev || [])]);
|
||||
|
||||
return newForecasts;
|
||||
} catch (error) {
|
||||
@@ -52,7 +52,7 @@ export const useForecast = () => {
|
||||
setError(null);
|
||||
|
||||
const batchForecast = await forecastingService.createBatchForecast(tenantId, request);
|
||||
setBatchForecasts(prev => [batchForecast, ...prev]);
|
||||
setBatchForecasts(prev => [batchForecast, ...(prev || [])]);
|
||||
|
||||
return batchForecast;
|
||||
} catch (error) {
|
||||
@@ -90,7 +90,7 @@ export const useForecast = () => {
|
||||
const batchForecast = await forecastingService.getBatchForecastStatus(tenantId, batchId);
|
||||
|
||||
// Update batch forecast in state
|
||||
setBatchForecasts(prev => prev.map(bf =>
|
||||
setBatchForecasts(prev => (prev || []).map(bf =>
|
||||
bf.id === batchId ? batchForecast : bf
|
||||
));
|
||||
|
||||
@@ -147,7 +147,7 @@ export const useForecast = () => {
|
||||
setError(null);
|
||||
|
||||
const acknowledgedAlert = await forecastingService.acknowledgeForecastAlert(tenantId, alertId);
|
||||
setAlerts(prev => prev.map(alert =>
|
||||
setAlerts(prev => (prev || []).map(alert =>
|
||||
alert.id === alertId ? acknowledgedAlert : alert
|
||||
));
|
||||
} catch (error) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
PaginatedResponse,
|
||||
InventoryDashboardData
|
||||
} from '../services/inventory.service';
|
||||
import type { ProductInfo } from '../types';
|
||||
|
||||
import { useTenantId } from '../../hooks/useTenantId';
|
||||
|
||||
@@ -117,17 +118,29 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
|
||||
try {
|
||||
const response = await inventoryService.getInventoryItems(tenantId, params);
|
||||
setItems(response.items);
|
||||
console.log('🔄 useInventory: Loaded items:', response.items);
|
||||
setItems(response.items || []); // Ensure it's always an array
|
||||
setPagination({
|
||||
page: response.page,
|
||||
limit: response.limit,
|
||||
total: response.total,
|
||||
totalPages: response.total_pages
|
||||
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);
|
||||
toast.error(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);
|
||||
}
|
||||
@@ -222,6 +235,7 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
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]);
|
||||
|
||||
@@ -273,6 +287,7 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
setAlerts(alertsData);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading alerts:', err);
|
||||
// Don't show toast error for this as it's not critical for forecast page
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
@@ -301,6 +316,7 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
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]);
|
||||
|
||||
@@ -507,4 +523,61 @@ export const useInventoryItem = (itemId: string): UseInventoryItemReturn => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -156,25 +156,6 @@ export const useSales = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Products List
|
||||
*/
|
||||
const getProductsList = useCallback(async (tenantId: string): Promise<string[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const products = await salesService.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 Sales Analytics
|
||||
@@ -213,7 +194,6 @@ export const useSales = () => {
|
||||
getDashboardStats,
|
||||
getRecentActivity,
|
||||
exportSalesData,
|
||||
getProductsList,
|
||||
getSalesAnalytics,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
trainingService,
|
||||
forecastingService,
|
||||
notificationService,
|
||||
inventoryService,
|
||||
api
|
||||
} from './services';
|
||||
|
||||
@@ -31,6 +32,8 @@ export {
|
||||
useNotification,
|
||||
useApiHooks,
|
||||
useOnboarding,
|
||||
useInventory,
|
||||
useInventoryProducts
|
||||
} from './hooks';
|
||||
|
||||
// Export WebSocket functionality
|
||||
|
||||
@@ -80,6 +80,7 @@ export class HealthService {
|
||||
{ name: 'Sales', endpoint: '/sales/health' },
|
||||
{ name: 'External', endpoint: '/external/health' },
|
||||
{ name: 'Training', endpoint: '/training/health' },
|
||||
{ name: 'Inventory', endpoint: '/inventory/health' },
|
||||
{ name: 'Forecasting', endpoint: '/forecasting/health' },
|
||||
{ name: 'Notification', endpoint: '/notifications/health' },
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
import type { ProductInfo } from '../types';
|
||||
|
||||
// ========== TYPES AND INTERFACES ==========
|
||||
|
||||
@@ -208,7 +209,7 @@ export interface PaginatedResponse<T> {
|
||||
// ========== INVENTORY SERVICE CLASS ==========
|
||||
|
||||
export class InventoryService {
|
||||
private baseEndpoint = '/api/v1';
|
||||
private baseEndpoint = '';
|
||||
|
||||
// ========== INVENTORY ITEMS ==========
|
||||
|
||||
@@ -230,16 +231,70 @@ export class InventoryService {
|
||||
}
|
||||
|
||||
const query = searchParams.toString();
|
||||
const url = `${this.baseEndpoint}/tenants/${tenantId}/inventory/items${query ? `?${query}` : ''}`;
|
||||
const url = `/tenants/${tenantId}/ingredients${query ? `?${query}` : ''}`;
|
||||
|
||||
return apiClient.get(url);
|
||||
console.log('🔍 InventoryService: Fetching inventory items from:', url);
|
||||
|
||||
try {
|
||||
console.log('🔑 InventoryService: Making request with auth token:', localStorage.getItem('auth_token') ? 'Present' : 'Missing');
|
||||
const response = await apiClient.get(url);
|
||||
console.log('📋 InventoryService: Raw response:', response);
|
||||
console.log('📋 InventoryService: Response type:', typeof response);
|
||||
console.log('📋 InventoryService: Response keys:', response ? Object.keys(response) : 'null');
|
||||
|
||||
// Handle different response formats
|
||||
if (Array.isArray(response)) {
|
||||
// Direct array response
|
||||
console.log('✅ InventoryService: Array response with', response.length, 'items');
|
||||
return {
|
||||
items: response,
|
||||
total: response.length,
|
||||
page: 1,
|
||||
limit: response.length,
|
||||
total_pages: 1
|
||||
};
|
||||
} else if (response && typeof response === 'object') {
|
||||
// Check if it's already paginated
|
||||
if ('items' in response && Array.isArray(response.items)) {
|
||||
console.log('✅ InventoryService: Paginated response with', response.items.length, 'items');
|
||||
return response;
|
||||
}
|
||||
|
||||
// Handle object with numeric keys (convert to array)
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
|
||||
const items = Object.values(response);
|
||||
console.log('✅ InventoryService: Numeric keys response with', items.length, 'items');
|
||||
return {
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
limit: items.length,
|
||||
total_pages: 1
|
||||
};
|
||||
}
|
||||
|
||||
// Handle empty object - this seems to be what we're getting
|
||||
if (keys.length === 0) {
|
||||
console.log('📭 InventoryService: Empty object response - backend has no inventory items for this tenant');
|
||||
throw new Error('NO_INVENTORY_ITEMS'); // This will trigger fallback in useInventory
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: unexpected response format
|
||||
console.warn('⚠️ InventoryService: Unexpected response format, keys:', Object.keys(response || {}));
|
||||
throw new Error('UNEXPECTED_RESPONSE_FORMAT');
|
||||
} catch (error) {
|
||||
console.error('❌ InventoryService: Failed to fetch inventory items:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single inventory item by ID
|
||||
*/
|
||||
async getInventoryItem(tenantId: string, itemId: string): Promise<InventoryItem> {
|
||||
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/${itemId}`);
|
||||
return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,7 +304,7 @@ export class InventoryService {
|
||||
tenantId: string,
|
||||
data: CreateInventoryItemRequest
|
||||
): Promise<InventoryItem> {
|
||||
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items`, data);
|
||||
return apiClient.post(`/tenants/${tenantId}/ingredients`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,14 +315,14 @@ export class InventoryService {
|
||||
itemId: string,
|
||||
data: UpdateInventoryItemRequest
|
||||
): Promise<InventoryItem> {
|
||||
return apiClient.put(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/${itemId}`, data);
|
||||
return apiClient.put(`/tenants/${tenantId}/ingredients/${itemId}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete inventory item (soft delete)
|
||||
*/
|
||||
async deleteInventoryItem(tenantId: string, itemId: string): Promise<void> {
|
||||
return apiClient.delete(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/${itemId}`);
|
||||
return apiClient.delete(`/tenants/${tenantId}/ingredients/${itemId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,7 +332,7 @@ export class InventoryService {
|
||||
tenantId: string,
|
||||
updates: { id: string; data: UpdateInventoryItemRequest }[]
|
||||
): Promise<{ success: number; failed: number; errors: string[] }> {
|
||||
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/bulk-update`, {
|
||||
return apiClient.post(`/tenants/${tenantId}/ingredients/bulk-update`, {
|
||||
updates
|
||||
});
|
||||
}
|
||||
@@ -288,14 +343,16 @@ export class InventoryService {
|
||||
* Get current stock level for an item
|
||||
*/
|
||||
async getStockLevel(tenantId: string, itemId: string): Promise<StockLevel> {
|
||||
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/${itemId}`);
|
||||
return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}/stock`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock levels for all items
|
||||
*/
|
||||
async getAllStockLevels(tenantId: string): Promise<StockLevel[]> {
|
||||
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock`);
|
||||
// TODO: Map to correct endpoint when available
|
||||
return [];
|
||||
// return apiClient.get(`/stock/summary`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,7 +364,7 @@ export class InventoryService {
|
||||
adjustment: StockAdjustmentRequest
|
||||
): Promise<StockMovement> {
|
||||
return apiClient.post(
|
||||
`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/${itemId}/adjust`,
|
||||
`/stock/consume`,
|
||||
adjustment
|
||||
);
|
||||
}
|
||||
@@ -353,7 +410,9 @@ export class InventoryService {
|
||||
* Get current stock alerts
|
||||
*/
|
||||
async getStockAlerts(tenantId: string): Promise<StockAlert[]> {
|
||||
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/alerts`);
|
||||
// TODO: Map to correct endpoint when available
|
||||
return [];
|
||||
// return apiClient.get(`/tenants/${tenantId}/inventory/alerts`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,7 +437,17 @@ export class InventoryService {
|
||||
* Get inventory dashboard data
|
||||
*/
|
||||
async getDashboardData(tenantId: string): Promise<InventoryDashboardData> {
|
||||
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/dashboard`);
|
||||
// TODO: Map to correct endpoint when available
|
||||
return {
|
||||
total_items: 0,
|
||||
low_stock_items: 0,
|
||||
out_of_stock_items: 0,
|
||||
total_value: 0,
|
||||
recent_movements: [],
|
||||
top_products: [],
|
||||
stock_alerts: []
|
||||
};
|
||||
// return apiClient.get(`/tenants/${tenantId}/inventory/dashboard`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,6 +539,87 @@ export class InventoryService {
|
||||
async getSupplierSuggestions(tenantId: string): Promise<string[]> {
|
||||
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/suppliers`);
|
||||
}
|
||||
|
||||
// ========== PRODUCTS FOR FORECASTING ==========
|
||||
|
||||
/**
|
||||
* Get Products List with IDs for Forecasting
|
||||
*/
|
||||
async getProductsList(tenantId: string): Promise<ProductInfo[]> {
|
||||
try {
|
||||
const response = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
|
||||
params: { limit: 100 }, // Get all products
|
||||
});
|
||||
|
||||
console.log('🔍 Inventory Products API Response:', response);
|
||||
|
||||
let productsArray: any[] = [];
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
productsArray = response;
|
||||
} else if (response && typeof response === 'object') {
|
||||
// Handle different response formats
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
|
||||
productsArray = Object.values(response);
|
||||
} else {
|
||||
console.warn('⚠️ Response is object but not with numeric keys:', response);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Response is not array or object:', response);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert to ProductInfo objects
|
||||
const products: ProductInfo[] = productsArray
|
||||
.map((product: any) => ({
|
||||
inventory_product_id: product.id || product.inventory_product_id,
|
||||
name: product.name || product.product_name || `Product ${product.id || ''}`,
|
||||
category: product.category,
|
||||
// Add additional fields if available from inventory
|
||||
current_stock: product.current_stock,
|
||||
unit: product.unit,
|
||||
cost_per_unit: product.cost_per_unit
|
||||
}))
|
||||
.filter(product => product.inventory_product_id && product.name);
|
||||
|
||||
console.log('📋 Processed inventory products:', products);
|
||||
|
||||
return products;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch inventory products:', error);
|
||||
|
||||
// Return empty array on error - let dashboard handle fallback
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Product by ID
|
||||
*/
|
||||
async getProductById(tenantId: string, productId: string): Promise<ProductInfo | null> {
|
||||
try {
|
||||
const response = await apiClient.get(`/tenants/${tenantId}/ingredients/${productId}`);
|
||||
|
||||
if (response) {
|
||||
return {
|
||||
inventory_product_id: response.id || response.inventory_product_id,
|
||||
name: response.name || response.product_name,
|
||||
category: response.category,
|
||||
current_stock: response.current_stock,
|
||||
unit: response.unit,
|
||||
cost_per_unit: response.cost_per_unit
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch product by ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const inventoryService = new InventoryService();
|
||||
@@ -181,85 +181,6 @@ export class SalesService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Products List from Sales Data
|
||||
*/
|
||||
async getProductsList(tenantId: string): Promise<string[]> {
|
||||
try {
|
||||
const response = await apiClient.get(`/tenants/${tenantId}/sales/products`);
|
||||
|
||||
console.log('🔍 Products API Response Analysis:');
|
||||
console.log('- Type:', typeof response);
|
||||
console.log('- Is Array:', Array.isArray(response));
|
||||
console.log('- Keys:', Object.keys(response || {}));
|
||||
console.log('- Response:', response);
|
||||
|
||||
let productsArray: any[] = [];
|
||||
|
||||
// ✅ FIX: Handle different response formats
|
||||
if (Array.isArray(response)) {
|
||||
// Standard array response
|
||||
productsArray = response;
|
||||
console.log('✅ Response is already an array');
|
||||
} else if (response && typeof response === 'object') {
|
||||
// Object with numeric keys - convert to array
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
|
||||
// Object has numeric keys like {0: {...}, 1: {...}}
|
||||
productsArray = Object.values(response);
|
||||
console.log('✅ Converted object with numeric keys to array');
|
||||
} else {
|
||||
console.warn('⚠️ Response is object but not with numeric keys:', response);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Response is not array or object:', response);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('📦 Products array:', productsArray);
|
||||
|
||||
// Extract product names from the array
|
||||
const productNames = productsArray
|
||||
.map((product: any) => {
|
||||
if (typeof product === 'string') {
|
||||
return product;
|
||||
}
|
||||
if (product && typeof product === 'object') {
|
||||
return product.product_name ||
|
||||
product.name ||
|
||||
product.productName ||
|
||||
null;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) // Remove null/undefined values
|
||||
.filter((name: string) => name.trim().length > 0); // Remove empty strings
|
||||
|
||||
console.log('📋 Extracted product names:', productNames);
|
||||
|
||||
if (productNames.length === 0) {
|
||||
console.warn('⚠️ No valid product names extracted from response');
|
||||
}
|
||||
|
||||
return productNames;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch products list:', error);
|
||||
|
||||
// Return fallback products for Madrid bakery
|
||||
return [
|
||||
'Croissants',
|
||||
'Pan de molde',
|
||||
'Baguettes',
|
||||
'Café',
|
||||
'Napolitanas',
|
||||
'Pan integral',
|
||||
'Magdalenas',
|
||||
'Churros'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Sales Summary by Period
|
||||
|
||||
@@ -91,10 +91,21 @@ export class TenantService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User's Tenants
|
||||
* Get User's Tenants - Get tenants where user is owner
|
||||
*/
|
||||
async getUserTenants(): Promise<TenantInfo[]> {
|
||||
return apiClient.get(`/users/me/tenants`);
|
||||
try {
|
||||
// First get current user info to get user ID
|
||||
const currentUser = await apiClient.get(`/users/me`);
|
||||
const userId = currentUser.id;
|
||||
|
||||
// Then get tenants owned by this user
|
||||
return apiClient.get(`${this.baseEndpoint}/user/${userId}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to get user tenants:', error);
|
||||
// Return empty array if API call fails
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
|
||||
import { BaseQueryParams } from './common';
|
||||
|
||||
export interface ProductInfo {
|
||||
inventory_product_id: string;
|
||||
name: string;
|
||||
category?: string;
|
||||
sales_count?: number;
|
||||
total_quantity?: number;
|
||||
last_sale_date?: string;
|
||||
}
|
||||
|
||||
export interface SalesData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
@@ -26,7 +35,6 @@ export interface SalesData {
|
||||
cost_of_goods?: number;
|
||||
revenue?: number;
|
||||
quantity_sold?: number;
|
||||
inventory_product_id?: string;
|
||||
discount_applied?: number;
|
||||
weather_condition?: string;
|
||||
}
|
||||
|
||||
@@ -97,20 +97,48 @@ export const useWebSocket = (config: WebSocketConfig) => {
|
||||
// Hook for training job updates
|
||||
export const useTrainingWebSocket = (jobId: string, tenantId?: string) => {
|
||||
const [jobUpdates, setJobUpdates] = useState<any[]>([]);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [isAuthenticationError, setIsAuthenticationError] = useState(false);
|
||||
|
||||
// Get tenant ID reliably
|
||||
// Get tenant ID reliably with enhanced error handling
|
||||
const actualTenantId = tenantId || (() => {
|
||||
try {
|
||||
const userData = localStorage.getItem('user_data');
|
||||
if (userData) {
|
||||
const parsed = JSON.parse(userData);
|
||||
return parsed.current_tenant_id || parsed.tenant_id;
|
||||
}
|
||||
// Try multiple sources for tenant ID
|
||||
const sources = [
|
||||
() => localStorage.getItem('current_tenant_id'),
|
||||
() => {
|
||||
const userData = localStorage.getItem('user_data');
|
||||
if (userData) {
|
||||
const parsed = JSON.parse(userData);
|
||||
return parsed.current_tenant_id || parsed.tenant_id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
() => {
|
||||
const authData = localStorage.getItem('auth_data');
|
||||
if (authData) {
|
||||
const parsed = JSON.parse(authData);
|
||||
return parsed.tenant_id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
() => {
|
||||
const tenantContext = localStorage.getItem('tenant_context');
|
||||
if (tenantContext) {
|
||||
const parsed = JSON.parse(tenantContext);
|
||||
return parsed.current_tenant_id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
];
|
||||
|
||||
const authData = localStorage.getItem('auth_data');
|
||||
if (authData) {
|
||||
const parsed = JSON.parse(authData);
|
||||
return parsed.tenant_id;
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const tenantId = source();
|
||||
if (tenantId) return tenantId;
|
||||
} catch (e) {
|
||||
console.warn('Failed to get tenant ID from source:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse tenant ID from storage:', e);
|
||||
@@ -123,8 +151,10 @@ export const useTrainingWebSocket = (jobId: string, tenantId?: string) => {
|
||||
? `ws://localhost:8000/api/v1/ws/tenants/${actualTenantId}/training/jobs/${jobId}/live`
|
||||
: `ws://localhost:8000/api/v1/ws/tenants/unknown/training/jobs/${jobId}/live`,
|
||||
reconnect: true,
|
||||
reconnectInterval: 3000,
|
||||
maxReconnectAttempts: 10
|
||||
reconnectInterval: 3000, // Faster reconnection for training
|
||||
maxReconnectAttempts: 20, // More attempts for long training jobs
|
||||
heartbeatInterval: 15000, // Send heartbeat every 15 seconds for training jobs
|
||||
enableLogging: true // Enable logging for debugging
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -137,40 +167,93 @@ export const useTrainingWebSocket = (jobId: string, tenantId?: string) => {
|
||||
sendMessage
|
||||
} = useWebSocket(config);
|
||||
|
||||
// Enhanced message handler
|
||||
// Enhanced message handler with error handling
|
||||
const handleWebSocketMessage = useCallback((message: any) => {
|
||||
// Handle different message structures
|
||||
let processedMessage = message;
|
||||
|
||||
// If message has nested data, flatten it for easier processing
|
||||
if (message.data && typeof message.data === 'object') {
|
||||
processedMessage = {
|
||||
...message,
|
||||
// Merge data properties to root level for backward compatibility
|
||||
...message.data
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Clear connection error when receiving messages
|
||||
setConnectionError(null);
|
||||
setIsAuthenticationError(false);
|
||||
|
||||
// Handle different message structures
|
||||
let processedMessage = message;
|
||||
|
||||
// If message has nested data, flatten it for easier processing
|
||||
if (message.data && typeof message.data === 'object') {
|
||||
processedMessage = {
|
||||
...message,
|
||||
// Merge data properties to root level for backward compatibility
|
||||
...message.data,
|
||||
// Preserve original structure
|
||||
_originalData: message.data
|
||||
};
|
||||
}
|
||||
|
||||
// Comprehensive message type handling
|
||||
const trainingMessageTypes = [
|
||||
'progress', 'training_progress',
|
||||
'completed', 'training_completed',
|
||||
'failed', 'training_failed',
|
||||
'error', 'training_error',
|
||||
'started', 'training_started',
|
||||
'heartbeat', 'initial_status',
|
||||
'status_update'
|
||||
];
|
||||
|
||||
if (trainingMessageTypes.includes(message.type)) {
|
||||
// Add to updates array with processed message
|
||||
setJobUpdates(prev => {
|
||||
const newUpdates = [processedMessage, ...prev.slice(0, 49)]; // Keep last 50 messages
|
||||
return newUpdates;
|
||||
});
|
||||
} else {
|
||||
// Still add to updates for debugging purposes
|
||||
setJobUpdates(prev => [processedMessage, ...prev.slice(0, 49)]);
|
||||
// Handle special message types
|
||||
if (message.type === 'connection_established') {
|
||||
console.log('WebSocket training connection established:', message);
|
||||
setJobUpdates(prev => [{
|
||||
type: 'connection_established',
|
||||
message: 'Connected to training service',
|
||||
timestamp: Date.now()
|
||||
}, ...prev.slice(0, 49)]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle keepalive messages (don't show to user, just for connection health)
|
||||
if (message.type === 'pong' || message.type === 'heartbeat') {
|
||||
console.debug('Training WebSocket keepalive received:', message.type);
|
||||
return; // Don't add to jobUpdates
|
||||
}
|
||||
|
||||
if (message.type === 'authentication_error' || message.type === 'authorization_error') {
|
||||
console.error('WebSocket auth/authorization error:', message);
|
||||
setIsAuthenticationError(true);
|
||||
setConnectionError(message.message || 'Authentication/authorization failed - please refresh and try again');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'connection_error') {
|
||||
console.error('WebSocket connection error:', message);
|
||||
setConnectionError(message.message || 'Connection error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'connection_timeout') {
|
||||
console.warn('WebSocket connection timeout:', message);
|
||||
// Don't set as error, just log - connection will retry
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'job_not_found') {
|
||||
console.error('Training job not found:', message);
|
||||
setConnectionError('Training job not found. Please restart the training process.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Comprehensive message type handling
|
||||
const trainingMessageTypes = [
|
||||
'progress', 'training_progress',
|
||||
'completed', 'training_completed',
|
||||
'failed', 'training_failed',
|
||||
'error', 'training_error',
|
||||
'started', 'training_started',
|
||||
'heartbeat', 'initial_status',
|
||||
'status_update'
|
||||
];
|
||||
|
||||
if (trainingMessageTypes.includes(message.type)) {
|
||||
// Add to updates array with processed message
|
||||
setJobUpdates(prev => {
|
||||
const newUpdates = [processedMessage, ...prev.slice(0, 49)]; // Keep last 50 messages
|
||||
return newUpdates;
|
||||
});
|
||||
} else {
|
||||
// Still add to updates for debugging purposes
|
||||
console.log('Received unknown message type:', message.type, message);
|
||||
setJobUpdates(prev => [processedMessage, ...prev.slice(0, 49)]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error, message);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -179,21 +262,108 @@ export const useTrainingWebSocket = (jobId: string, tenantId?: string) => {
|
||||
addMessageHandler(handleWebSocketMessage);
|
||||
}, [addMessageHandler, handleWebSocketMessage]);
|
||||
|
||||
// Send periodic ping to keep connection alive
|
||||
// Enhanced dual ping system for training jobs - prevent disconnection during long training
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
const pingInterval = setInterval(() => {
|
||||
sendMessage({
|
||||
type: 'ping',
|
||||
data: undefined
|
||||
// Primary ping system using JSON messages with training info
|
||||
const keepaliveInterval = setInterval(() => {
|
||||
const success = sendMessage({
|
||||
type: 'training_keepalive',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
job_id: jobId,
|
||||
tenant_id: actualTenantId,
|
||||
status: 'active'
|
||||
}
|
||||
});
|
||||
}, 30000); // Every 30 seconds
|
||||
|
||||
if (!success) {
|
||||
console.warn('Training keepalive failed - connection may be lost');
|
||||
}
|
||||
}, 10000); // Every 10 seconds for training jobs
|
||||
|
||||
// Secondary simple text ping system (more lightweight)
|
||||
const simplePingInterval = setInterval(() => {
|
||||
// Send a simple text ping to keep connection alive
|
||||
const success = sendMessage({
|
||||
type: 'ping',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
source: 'training_client'
|
||||
}
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
console.warn('Simple training ping failed');
|
||||
}
|
||||
}, 15000); // Every 15 seconds
|
||||
|
||||
return () => {
|
||||
clearInterval(pingInterval);
|
||||
clearInterval(keepaliveInterval);
|
||||
clearInterval(simplePingInterval);
|
||||
};
|
||||
}
|
||||
}, [isConnected, sendMessage]);
|
||||
}, [isConnected, sendMessage, jobId, actualTenantId]);
|
||||
|
||||
// Define refresh connection function
|
||||
const refreshConnection = useCallback(() => {
|
||||
setConnectionError(null);
|
||||
setIsAuthenticationError(false);
|
||||
disconnect();
|
||||
setTimeout(() => {
|
||||
connect();
|
||||
}, 1000);
|
||||
}, [connect, disconnect]);
|
||||
|
||||
// Enhanced connection monitoring and auto-recovery for training jobs
|
||||
useEffect(() => {
|
||||
if (actualTenantId && jobId !== 'pending') {
|
||||
const healthCheckInterval = setInterval(() => {
|
||||
// If we should be connected but aren't, try to reconnect
|
||||
if (status === 'disconnected' && !connectionError) {
|
||||
console.log('WebSocket health check: reconnecting disconnected training socket');
|
||||
connect();
|
||||
}
|
||||
|
||||
// More aggressive stale connection detection for training jobs
|
||||
const lastUpdate = jobUpdates.length > 0 ? jobUpdates[0] : null;
|
||||
if (lastUpdate && status === 'connected') {
|
||||
const timeSinceLastMessage = Date.now() - (lastUpdate.timestamp || 0);
|
||||
if (timeSinceLastMessage > 45000) { // 45 seconds without messages during training
|
||||
console.log('WebSocket health check: connection appears stale, refreshing');
|
||||
refreshConnection();
|
||||
}
|
||||
}
|
||||
|
||||
// If connection is in a failed state for too long, force reconnect
|
||||
if (status === 'failed' && !isAuthenticationError) {
|
||||
console.log('WebSocket health check: recovering from failed state');
|
||||
setTimeout(() => connect(), 2000);
|
||||
}
|
||||
}, 12000); // Check every 12 seconds for training jobs
|
||||
|
||||
return () => clearInterval(healthCheckInterval);
|
||||
}
|
||||
}, [actualTenantId, jobId, status, connectionError, connect, refreshConnection, jobUpdates, isAuthenticationError]);
|
||||
|
||||
// Enhanced connection setup - request current status when connecting
|
||||
useEffect(() => {
|
||||
if (isConnected && jobId !== 'pending') {
|
||||
// Wait a moment for connection to stabilize, then request current status
|
||||
const statusRequestTimer = setTimeout(() => {
|
||||
console.log('Requesting current training status after connection');
|
||||
sendMessage({
|
||||
type: 'get_status',
|
||||
data: {
|
||||
job_id: jobId,
|
||||
tenant_id: actualTenantId
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(statusRequestTimer);
|
||||
}
|
||||
}, [isConnected, jobId, actualTenantId, sendMessage]);
|
||||
|
||||
return {
|
||||
status,
|
||||
@@ -204,13 +374,33 @@ export const useTrainingWebSocket = (jobId: string, tenantId?: string) => {
|
||||
lastMessage,
|
||||
tenantId: actualTenantId,
|
||||
wsUrl: config.url,
|
||||
// Manual refresh function
|
||||
refreshConnection: useCallback(() => {
|
||||
connectionError,
|
||||
isAuthenticationError,
|
||||
// Enhanced refresh function with status request
|
||||
refreshConnection,
|
||||
// Force retry with new authentication
|
||||
retryWithAuth: useCallback(() => {
|
||||
setConnectionError(null);
|
||||
setIsAuthenticationError(false);
|
||||
// Clear any cached auth data that might be stale
|
||||
disconnect();
|
||||
setTimeout(() => {
|
||||
connect();
|
||||
}, 1000);
|
||||
}, [connect, disconnect])
|
||||
}, 2000);
|
||||
}, [connect, disconnect]),
|
||||
// Manual status request function
|
||||
requestStatus: useCallback(() => {
|
||||
if (isConnected && jobId !== 'pending') {
|
||||
return sendMessage({
|
||||
type: 'get_status',
|
||||
data: {
|
||||
job_id: jobId,
|
||||
tenant_id: actualTenantId
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}, [isConnected, jobId, actualTenantId, sendMessage])
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -99,8 +99,17 @@ export class WebSocketManager {
|
||||
this.handlers.onClose?.(event);
|
||||
|
||||
// Auto-reconnect if enabled and not manually closed
|
||||
if (this.config.reconnect && event.code !== 1000) {
|
||||
// Don't reconnect on authorization failures or job not found (1008) with specific reasons
|
||||
const isAuthorizationError = event.code === 1008 &&
|
||||
(event.reason === 'Authentication failed' || event.reason === 'Authorization failed');
|
||||
const isJobNotFound = event.code === 1008 && event.reason === 'Job not found';
|
||||
|
||||
if (this.config.reconnect && event.code !== 1000 && !isAuthorizationError && !isJobNotFound) {
|
||||
this.scheduleReconnect();
|
||||
} else if (isAuthorizationError || isJobNotFound) {
|
||||
this.log('Connection failed - stopping reconnection attempts:', event.reason);
|
||||
this.status = 'failed';
|
||||
this.handlers.onReconnectFailed?.();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user