Improve AI logic
This commit is contained in:
446
frontend/src/api/services/aiInsights.ts
Normal file
446
frontend/src/api/services/aiInsights.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* AI Insights Service
|
||||
*
|
||||
* Provides access to AI-generated insights from the AI Insights microservice.
|
||||
* Replaces mock data with real API integration.
|
||||
*
|
||||
* Backend endpoints:
|
||||
* - GET /tenants/{tenant_id}/insights
|
||||
* - GET /tenants/{tenant_id}/insights/{insight_id}
|
||||
* - POST /tenants/{tenant_id}/insights/feedback
|
||||
* - GET /tenants/{tenant_id}/insights/stats
|
||||
* - GET /tenants/{tenant_id}/insights/orchestration-ready
|
||||
*
|
||||
* Last Updated: 2025-11-03
|
||||
* Status: ✅ Complete - Real API Integration
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export interface AIInsight {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
type: 'forecast' | 'warning' | 'opportunity' | 'positive' | 'optimization' | 'rule';
|
||||
priority: 'urgent' | 'high' | 'medium' | 'low';
|
||||
category: 'demand' | 'procurement' | 'inventory' | 'production' | 'sales' | 'system' | 'business';
|
||||
title: string;
|
||||
description: string;
|
||||
impact_type: 'cost_savings' | 'waste_reduction' | 'yield_improvement' | 'revenue' | 'system_health' | 'process_improvement';
|
||||
impact_value?: number;
|
||||
impact_unit?: string;
|
||||
confidence: number;
|
||||
metrics_json: Record<string, any>;
|
||||
actionable: boolean;
|
||||
recommendation_actions?: Array<{
|
||||
label: string;
|
||||
action: string;
|
||||
params: Record<string, any>;
|
||||
}>;
|
||||
source_service: string;
|
||||
source_model: string;
|
||||
detected_at: string;
|
||||
resolved_at?: string;
|
||||
resolved_by?: string;
|
||||
status: 'active' | 'applied' | 'dismissed' | 'resolved';
|
||||
feedback_count?: number;
|
||||
avg_feedback_rating?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AIInsightFilters {
|
||||
type?: string;
|
||||
priority?: string;
|
||||
category?: string;
|
||||
source_model?: string;
|
||||
status?: string;
|
||||
min_confidence?: number;
|
||||
actionable_only?: boolean;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface AIInsightListResponse {
|
||||
items: AIInsight[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface AIInsightStatsResponse {
|
||||
total_insights: number;
|
||||
insights_by_type: Record<string, number>;
|
||||
insights_by_priority: Record<string, number>;
|
||||
insights_by_category: Record<string, number>;
|
||||
insights_by_status: Record<string, number>;
|
||||
avg_confidence: number;
|
||||
total_impact_value: number;
|
||||
actionable_insights: number;
|
||||
resolved_insights: number;
|
||||
}
|
||||
|
||||
export interface FeedbackRequest {
|
||||
applied: boolean;
|
||||
applied_at?: string;
|
||||
outcome_date?: string;
|
||||
outcome_metrics?: Record<string, any>;
|
||||
user_rating?: number;
|
||||
user_comment?: string;
|
||||
}
|
||||
|
||||
export interface FeedbackResponse {
|
||||
insight_id: string;
|
||||
feedback_recorded: boolean;
|
||||
feedback_id: string;
|
||||
recorded_at: string;
|
||||
}
|
||||
|
||||
export interface OrchestrationReadyInsightsRequest {
|
||||
target_date: string;
|
||||
min_confidence?: number;
|
||||
}
|
||||
|
||||
export interface OrchestrationReadyInsightsResponse {
|
||||
target_date: string;
|
||||
insights: AIInsight[];
|
||||
categorized_insights: {
|
||||
demand_forecasts: AIInsight[];
|
||||
supplier_alerts: AIInsight[];
|
||||
inventory_optimizations: AIInsight[];
|
||||
price_opportunities: AIInsight[];
|
||||
yield_predictions: AIInsight[];
|
||||
business_rules: AIInsight[];
|
||||
other: AIInsight[];
|
||||
};
|
||||
total_insights: number;
|
||||
}
|
||||
|
||||
export class AIInsightsService {
|
||||
private readonly baseUrl = '/tenants';
|
||||
|
||||
/**
|
||||
* Get all AI insights for a tenant with optional filters
|
||||
*/
|
||||
async getInsights(
|
||||
tenantId: string,
|
||||
filters?: AIInsightFilters
|
||||
): Promise<AIInsightListResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (filters?.type) queryParams.append('type', filters.type);
|
||||
if (filters?.priority) queryParams.append('priority', filters.priority);
|
||||
if (filters?.category) queryParams.append('category', filters.category);
|
||||
if (filters?.source_model) queryParams.append('source_model', filters.source_model);
|
||||
if (filters?.status) queryParams.append('status', filters.status);
|
||||
if (filters?.min_confidence) queryParams.append('min_confidence', filters.min_confidence.toString());
|
||||
if (filters?.actionable_only) queryParams.append('actionable_only', 'true');
|
||||
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
|
||||
if (filters?.search) queryParams.append('search', filters.search);
|
||||
if (filters?.limit) queryParams.append('limit', filters.limit.toString());
|
||||
if (filters?.offset) queryParams.append('offset', filters.offset.toString());
|
||||
|
||||
const url = `${this.baseUrl}/${tenantId}/insights${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||||
|
||||
return apiClient.get<AIInsightListResponse>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single insight by ID
|
||||
*/
|
||||
async getInsight(
|
||||
tenantId: string,
|
||||
insightId: string
|
||||
): Promise<AIInsight> {
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}`;
|
||||
return apiClient.get<AIInsight>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insight statistics
|
||||
*/
|
||||
async getInsightStats(
|
||||
tenantId: string,
|
||||
filters?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
): Promise<AIInsightStatsResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
|
||||
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/metrics/summary${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||||
|
||||
return apiClient.get<AIInsightStatsResponse>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestration-ready insights for a specific date
|
||||
*/
|
||||
async getOrchestrationReadyInsights(
|
||||
tenantId: string,
|
||||
request: OrchestrationReadyInsightsRequest
|
||||
): Promise<OrchestrationReadyInsightsResponse> {
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/orchestration-ready`;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('target_date', request.target_date);
|
||||
if (request.min_confidence) {
|
||||
queryParams.append('min_confidence', request.min_confidence.toString());
|
||||
}
|
||||
|
||||
return apiClient.get<OrchestrationReadyInsightsResponse>(
|
||||
`${url}?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record feedback for an applied insight
|
||||
*/
|
||||
async recordFeedback(
|
||||
tenantId: string,
|
||||
insightId: string,
|
||||
feedback: FeedbackRequest
|
||||
): Promise<FeedbackResponse> {
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/feedback`;
|
||||
return apiClient.post<FeedbackResponse>(url, feedback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an insight (mark as applied)
|
||||
*/
|
||||
async applyInsight(
|
||||
tenantId: string,
|
||||
insightId: string
|
||||
): Promise<AIInsight> {
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/apply`;
|
||||
return apiClient.post<AIInsight>(url, {
|
||||
applied_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an insight
|
||||
*/
|
||||
async dismissInsight(
|
||||
tenantId: string,
|
||||
insightId: string,
|
||||
reason?: string
|
||||
): Promise<AIInsight> {
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/dismiss`;
|
||||
return apiClient.post<AIInsight>(url, { reason });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an insight
|
||||
*/
|
||||
async resolveInsight(
|
||||
tenantId: string,
|
||||
insightId: string,
|
||||
resolution?: string
|
||||
): Promise<AIInsight> {
|
||||
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/resolve`;
|
||||
return apiClient.post<AIInsight>(url, { resolution });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insights by priority (for dashboard widgets)
|
||||
*/
|
||||
async getHighPriorityInsights(
|
||||
tenantId: string,
|
||||
limit: number = 10
|
||||
): Promise<AIInsight[]> {
|
||||
const response = await this.getInsights(tenantId, {
|
||||
priority: 'urgent',
|
||||
status: 'active',
|
||||
limit,
|
||||
});
|
||||
|
||||
if (response.items.length < limit) {
|
||||
// Add high priority if not enough urgent
|
||||
const highPriorityResponse = await this.getInsights(tenantId, {
|
||||
priority: 'high',
|
||||
status: 'active',
|
||||
limit: limit - response.items.length,
|
||||
});
|
||||
return [...response.items, ...highPriorityResponse.items];
|
||||
}
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actionable insights (for recommendations panel)
|
||||
*/
|
||||
async getActionableInsights(
|
||||
tenantId: string,
|
||||
limit: number = 20
|
||||
): Promise<AIInsight[]> {
|
||||
const response = await this.getInsights(tenantId, {
|
||||
actionable_only: true,
|
||||
status: 'active',
|
||||
limit,
|
||||
});
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insights by category
|
||||
*/
|
||||
async getInsightsByCategory(
|
||||
tenantId: string,
|
||||
category: string,
|
||||
limit: number = 20
|
||||
): Promise<AIInsight[]> {
|
||||
const response = await this.getInsights(tenantId, {
|
||||
category,
|
||||
status: 'active',
|
||||
limit,
|
||||
});
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search insights
|
||||
*/
|
||||
async searchInsights(
|
||||
tenantId: string,
|
||||
query: string,
|
||||
filters?: Partial<AIInsightFilters>
|
||||
): Promise<AIInsight[]> {
|
||||
const response = await this.getInsights(tenantId, {
|
||||
...filters,
|
||||
search: query,
|
||||
limit: filters?.limit || 50,
|
||||
});
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent insights (for activity feed)
|
||||
*/
|
||||
async getRecentInsights(
|
||||
tenantId: string,
|
||||
days: number = 7,
|
||||
limit: number = 50
|
||||
): Promise<AIInsight[]> {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - days);
|
||||
|
||||
const response = await this.getInsights(tenantId, {
|
||||
start_date: startDate.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
limit,
|
||||
});
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insights summary for dashboard
|
||||
*/
|
||||
async getDashboardSummary(
|
||||
tenantId: string
|
||||
): Promise<{
|
||||
stats: AIInsightStatsResponse;
|
||||
highPriority: AIInsight[];
|
||||
recent: AIInsight[];
|
||||
}> {
|
||||
const [stats, highPriority, recent] = await Promise.all([
|
||||
this.getInsightStats(tenantId),
|
||||
this.getHighPriorityInsights(tenantId, 5),
|
||||
this.getRecentInsights(tenantId, 7, 10),
|
||||
]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
highPriority,
|
||||
recent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format impact value for display
|
||||
*/
|
||||
formatImpactValue(insight: AIInsight): string {
|
||||
if (!insight.impact_value) return 'N/A';
|
||||
|
||||
const value = insight.impact_value;
|
||||
const unit = insight.impact_unit || 'units';
|
||||
|
||||
if (unit === 'euros_per_year' || unit === 'eur') {
|
||||
return `€${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}/year`;
|
||||
} else if (unit === 'euros') {
|
||||
return `€${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
} else if (unit === 'percentage' || unit === 'percentage_points') {
|
||||
return `${value.toFixed(1)}%`;
|
||||
} else if (unit === 'units') {
|
||||
return `${value.toFixed(0)} units`;
|
||||
} else {
|
||||
return `${value.toFixed(2)} ${unit}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority badge color
|
||||
*/
|
||||
getPriorityColor(priority: string): string {
|
||||
switch (priority) {
|
||||
case 'urgent':
|
||||
return 'red';
|
||||
case 'high':
|
||||
return 'orange';
|
||||
case 'medium':
|
||||
return 'yellow';
|
||||
case 'low':
|
||||
return 'blue';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type icon
|
||||
*/
|
||||
getTypeIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'forecast':
|
||||
return '📈';
|
||||
case 'warning':
|
||||
return '⚠️';
|
||||
case 'opportunity':
|
||||
return '💡';
|
||||
case 'positive':
|
||||
return '✅';
|
||||
case 'optimization':
|
||||
return '🎯';
|
||||
case 'rule':
|
||||
return '📋';
|
||||
default:
|
||||
return '📊';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate confidence color
|
||||
*/
|
||||
getConfidenceColor(confidence: number): string {
|
||||
if (confidence >= 90) return 'green';
|
||||
if (confidence >= 75) return 'blue';
|
||||
if (confidence >= 60) return 'yellow';
|
||||
return 'red';
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const aiInsightsService = new AIInsightsService();
|
||||
@@ -82,7 +82,7 @@ export class ProcurementService {
|
||||
|
||||
/**
|
||||
* Auto-generate procurement plan from forecast data (Orchestrator integration)
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/auto-generate
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/operations/auto-generate
|
||||
*
|
||||
* Called by Orchestrator Service to create procurement plans based on forecast data
|
||||
*/
|
||||
@@ -91,21 +91,21 @@ export class ProcurementService {
|
||||
request: AutoGenerateProcurementRequest
|
||||
): Promise<AutoGenerateProcurementResponse> {
|
||||
return apiClient.post<AutoGenerateProcurementResponse>(
|
||||
`/tenants/${tenantId}/procurement/auto-generate`,
|
||||
`/tenants/${tenantId}/procurement/operations/auto-generate`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new procurement plan (manual/UI-driven)
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans/generate
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans
|
||||
*/
|
||||
static async generateProcurementPlan(
|
||||
tenantId: string,
|
||||
request: GeneratePlanRequest
|
||||
): Promise<GeneratePlanResponse> {
|
||||
return apiClient.post<GeneratePlanResponse>(
|
||||
`/tenants/${tenantId}/procurement/plans/generate`,
|
||||
`/tenants/${tenantId}/procurement/plans`,
|
||||
request
|
||||
);
|
||||
}
|
||||
@@ -330,6 +330,121 @@ export class ProcurementService {
|
||||
{ auto_approve: autoApprove }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new purchase order
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/purchase-orders
|
||||
*/
|
||||
static async createPurchaseOrder(
|
||||
tenantId: string,
|
||||
poData: any
|
||||
): Promise<PurchaseOrderResponse> {
|
||||
return apiClient.post<PurchaseOrderResponse>(
|
||||
`/tenants/${tenantId}/procurement/purchase-orders`,
|
||||
poData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get purchase order by ID
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}
|
||||
*/
|
||||
static async getPurchaseOrderById(
|
||||
tenantId: string,
|
||||
poId: string
|
||||
): Promise<PurchaseOrderWithSupplierResponse> {
|
||||
return apiClient.get<PurchaseOrderWithSupplierResponse>(
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List purchase orders
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/purchase-orders
|
||||
*/
|
||||
static async getPurchaseOrders(
|
||||
tenantId: string,
|
||||
params?: { skip?: number; limit?: number; supplier_id?: string; status?: string }
|
||||
): Promise<PurchaseOrderResponse[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.skip !== undefined) queryParams.append('skip', params.skip.toString());
|
||||
if (params?.limit !== undefined) queryParams.append('limit', params.limit.toString());
|
||||
if (params?.supplier_id) queryParams.append('supplier_id', params.supplier_id);
|
||||
if (params?.status) queryParams.append('status', params.status);
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = `/tenants/${tenantId}/procurement/purchase-orders${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return apiClient.get<PurchaseOrderResponse[]>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update purchase order
|
||||
* PATCH /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}
|
||||
*/
|
||||
static async updatePurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
poData: any
|
||||
): Promise<PurchaseOrderResponse> {
|
||||
return apiClient.patch<PurchaseOrderResponse>(
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`,
|
||||
poData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update purchase order status
|
||||
* PATCH /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}/status
|
||||
*/
|
||||
static async updatePurchaseOrderStatus(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
status: string,
|
||||
notes?: string
|
||||
): Promise<PurchaseOrderResponse> {
|
||||
const queryParams = new URLSearchParams({ status });
|
||||
if (notes) queryParams.append('notes', notes);
|
||||
|
||||
return apiClient.patch<PurchaseOrderResponse>(
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/status?${queryParams.toString()}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve or reject purchase order
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}/approve
|
||||
*/
|
||||
static async approvePurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
approveData: any
|
||||
): Promise<PurchaseOrderResponse> {
|
||||
return apiClient.post<PurchaseOrderResponse>(
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/approve`,
|
||||
approveData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel purchase order
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}/cancel
|
||||
*/
|
||||
static async cancelPurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
reason: string,
|
||||
cancelledBy?: string
|
||||
): Promise<PurchaseOrderResponse> {
|
||||
const queryParams = new URLSearchParams({ reason });
|
||||
if (cancelledBy) queryParams.append('cancelled_by', cancelledBy);
|
||||
|
||||
return apiClient.post<PurchaseOrderResponse>(
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/cancel?${queryParams.toString()}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProcurementService;
|
||||
|
||||
Reference in New Issue
Block a user