Files
bakery-ia/frontend/src/api/services/forecasting.service.ts
2025-08-14 16:47:34 +02:00

301 lines
9.1 KiB
TypeScript

// 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<ForecastResponse[]> {
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<BatchForecastResponse> {
return apiClient.post(
`/tenants/${tenantId}/forecasts/batch`,
request,
{
timeout: RequestTimeouts.LONG,
}
);
}
/**
* Get Forecast by ID
*/
async getForecast(tenantId: string, forecastId: string): Promise<ForecastResponse> {
return apiClient.get(`/tenants/${tenantId}/forecasts/${forecastId}`);
}
/**
* Get Forecasts
*/
async getForecasts(
tenantId: string,
params?: BaseQueryParams & {
inventory_product_id?: string; // Primary way to filter by product
product_name?: string; // For backward compatibility - will need inventory service lookup
start_date?: string;
end_date?: string;
model_id?: string;
}
): Promise<PaginatedResponse<ForecastResponse>> {
return apiClient.get(`/tenants/${tenantId}/forecasts`, { params });
}
/**
* Get Batch Forecast Status
*/
async getBatchForecastStatus(
tenantId: string,
batchId: string
): Promise<BatchForecastResponse> {
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<PaginatedResponse<BatchForecastResponse>> {
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<QuickForecast[]> {
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) => ({
inventory_product_id: forecast.inventory_product_id,
product_name: forecast.product_name, // Optional - for display
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 (using mock inventory_product_ids)
return [
{
inventory_product_id: 'mock-pan-de-molde-001',
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()
},
{
inventory_product_id: 'mock-baguettes-002',
product_name: 'Baguettes',
next_day_prediction: 20,
next_week_avg: 140,
trend_direction: 'up',
confidence_score: 0.92,
last_updated: new Date().toISOString()
},
{
inventory_product_id: 'mock-croissants-003',
product_name: 'Croissants',
next_day_prediction: 15,
next_week_avg: 105,
trend_direction: 'stable',
confidence_score: 0.78,
last_updated: new Date().toISOString()
},
{
inventory_product_id: 'mock-magdalenas-004',
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<PaginatedResponse<ForecastAlert>> {
return apiClient.get(`/tenants/${tenantId}/forecasts/alerts`, { params });
}
/**
* Acknowledge Forecast Alert
*/
async acknowledgeForecastAlert(
tenantId: string,
alertId: string
): Promise<ForecastAlert> {
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?: {
inventory_product_id?: string; // Primary way to filter by product
product_name?: string; // For backward compatibility
start_date?: string;
end_date?: string;
}
): Promise<Blob> {
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?: {
inventory_product_id?: string; // Primary way to filter by product
product_name?: string; // For backward compatibility
model_id?: string;
start_date?: string;
end_date?: string;
}
): Promise<{
overall_accuracy: number;
product_accuracy: Array<{
inventory_product_id: string;
product_name?: string; // Optional - for display
accuracy: number;
sample_size: number;
}>;
}> {
return apiClient.get(`/tenants/${tenantId}/forecasts/accuracy`, { params });
}
}
export const forecastingService = new ForecastingService();