// frontend/src/api/services/forecasting.service.ts /** * Forecasting Service * Handles forecast operations and predictions */ import { apiClient } from '../client'; import { RequestTimeouts } from '../client/config'; import type { SingleForecastRequest, BatchForecastRequest, ForecastResponse, BatchForecastResponse, ForecastAlert, QuickForecast, PaginatedResponse, BaseQueryParams, } from '../types'; export class ForecastingService { /** * Create Single Product Forecast */ async createSingleForecast( tenantId: string, request: SingleForecastRequest ): Promise { console.log('🔮 Creating single forecast:', { tenantId, request }); try { // Backend returns single ForecastResponse object const response = await apiClient.post( `/tenants/${tenantId}/forecasts/single`, request, { timeout: RequestTimeouts.MEDIUM, } ); console.log('🔮 Forecast API Response:', response); console.log('- Type:', typeof response); console.log('- Is Array:', Array.isArray(response)); // ✅ FIX: Convert single response to array if (response && typeof response === 'object' && !Array.isArray(response)) { // Single forecast response - wrap in array const forecastArray = [response as ForecastResponse]; console.log('✅ Converted single forecast to array:', forecastArray); return forecastArray; } else if (Array.isArray(response)) { // Already an array (unexpected but handle gracefully) console.log('✅ Response is already an array:', response); return response; } else { console.error('❌ Unexpected response format:', response); throw new Error('Invalid forecast response format'); } } catch (error) { console.error('❌ Forecast API Error:', error); throw error; } } /** * Create Batch Forecast */ async createBatchForecast( tenantId: string, request: BatchForecastRequest ): Promise { return apiClient.post( `/tenants/${tenantId}/forecasts/batch`, request, { timeout: RequestTimeouts.LONG, } ); } /** * Get Forecast by ID */ async getForecast(tenantId: string, forecastId: string): Promise { return apiClient.get(`/tenants/${tenantId}/forecasts/${forecastId}`); } /** * Get Forecasts */ async getForecasts( tenantId: string, params?: BaseQueryParams & { product_name?: string; start_date?: string; end_date?: string; model_id?: string; } ): Promise> { return apiClient.get(`/tenants/${tenantId}/forecasts`, { params }); } /** * Get Batch Forecast Status */ async getBatchForecastStatus( tenantId: string, batchId: string ): Promise { return apiClient.get(`/tenants/${tenantId}/forecasts/batch/${batchId}/status`); } /** * Get Batch Forecasts */ async getBatchForecasts( tenantId: string, params?: BaseQueryParams & { status?: string; start_date?: string; end_date?: string; } ): Promise> { return apiClient.get(`/tenants/${tenantId}/forecasts/batch`, { params }); } /** * Cancel Batch Forecast */ async cancelBatchForecast(tenantId: string, batchId: string): Promise<{ message: string }> { return apiClient.post(`/tenants/${tenantId}/forecasts/batch/${batchId}/cancel`); } /** * Get Quick Forecasts for Dashboard */ async getQuickForecasts(tenantId: string, limit?: number): Promise { try { // TODO: Replace with actual /forecasts/quick endpoint when available // For now, use regular forecasts endpoint and transform the data const forecasts = await apiClient.get(`/tenants/${tenantId}/forecasts`, { params: { limit: limit || 10 }, }); // Transform regular forecasts to QuickForecast format // Handle response structure: { tenant_id, forecasts: [...], total_returned } let forecastsArray: any[] = []; if (Array.isArray(forecasts)) { // Direct array response (unexpected) forecastsArray = forecasts; } else if (forecasts && typeof forecasts === 'object' && Array.isArray(forecasts.forecasts)) { // Expected object response with forecasts array forecastsArray = forecasts.forecasts; } else { console.warn('Unexpected forecasts response format:', forecasts); return []; } return forecastsArray.map((forecast: any) => ({ product_name: forecast.product_name, next_day_prediction: forecast.predicted_demand || 0, next_week_avg: forecast.predicted_demand || 0, trend_direction: 'stable' as const, confidence_score: forecast.confidence_level || 0.8, last_updated: forecast.created_at || new Date().toISOString() })); } catch (error) { console.error('QuickForecasts API call failed, using fallback data:', error); // Return mock data for common bakery products return [ { product_name: 'Pan de Molde', next_day_prediction: 25, next_week_avg: 175, trend_direction: 'stable', confidence_score: 0.85, last_updated: new Date().toISOString() }, { product_name: 'Baguettes', next_day_prediction: 20, next_week_avg: 140, trend_direction: 'up', confidence_score: 0.92, last_updated: new Date().toISOString() }, { product_name: 'Croissants', next_day_prediction: 15, next_week_avg: 105, trend_direction: 'stable', confidence_score: 0.78, last_updated: new Date().toISOString() }, { product_name: 'Magdalenas', next_day_prediction: 12, next_week_avg: 84, trend_direction: 'down', confidence_score: 0.76, last_updated: new Date().toISOString() } ]; } } /** * Get Forecast Alerts */ async getForecastAlerts( tenantId: string, params?: BaseQueryParams & { is_active?: boolean; severity?: string; alert_type?: string; } ): Promise> { return apiClient.get(`/tenants/${tenantId}/forecasts/alerts`, { params }); } /** * Acknowledge Forecast Alert */ async acknowledgeForecastAlert( tenantId: string, alertId: string ): Promise { return apiClient.post(`/tenants/${tenantId}/forecasts/alerts/${alertId}/acknowledge`); } /** * Delete Forecast */ async deleteForecast(tenantId: string, forecastId: string): Promise<{ message: string }> { return apiClient.delete(`/tenants/${tenantId}/forecasts/${forecastId}`); } /** * Export Forecasts */ async exportForecasts( tenantId: string, format: 'csv' | 'excel' | 'json', params?: { product_name?: string; start_date?: string; end_date?: string; } ): Promise { const response = await apiClient.request(`/tenants/${tenantId}/forecasts/export`, { method: 'GET', params: { ...params, format }, headers: { 'Accept': format === 'csv' ? 'text/csv' : format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'application/json', }, }); return new Blob([response], { type: format === 'csv' ? 'text/csv' : format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'application/json', }); } /** * Get Forecast Accuracy Metrics */ async getForecastAccuracy( tenantId: string, params?: { product_name?: string; model_id?: string; start_date?: string; end_date?: string; } ): Promise<{ overall_accuracy: number; product_accuracy: Array<{ product_name: string; accuracy: number; sample_size: number; }>; }> { return apiClient.get(`/tenants/${tenantId}/forecasts/accuracy`, { params }); } } export const forecastingService = new ForecastingService();