301 lines
9.1 KiB
TypeScript
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(); |