Improve AI logic
This commit is contained in:
305
frontend/src/api/hooks/aiInsights.ts
Normal file
305
frontend/src/api/hooks/aiInsights.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* React Hooks for AI Insights
|
||||
*
|
||||
* Provides React Query hooks for AI Insights API integration.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { data: insights, isLoading } = useAIInsights(tenantId, { priority: 'high' });
|
||||
* const { data: stats } = useAIInsightStats(tenantId);
|
||||
* const applyMutation = useApplyInsight();
|
||||
* ```
|
||||
*
|
||||
* Last Updated: 2025-11-03
|
||||
* Status: ✅ Complete - React Query Integration
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import {
|
||||
aiInsightsService,
|
||||
AIInsight,
|
||||
AIInsightFilters,
|
||||
AIInsightListResponse,
|
||||
AIInsightStatsResponse,
|
||||
FeedbackRequest,
|
||||
OrchestrationReadyInsightsRequest,
|
||||
OrchestrationReadyInsightsResponse,
|
||||
} from '../services/aiInsights';
|
||||
|
||||
// Query Keys
|
||||
export const aiInsightsKeys = {
|
||||
all: ['aiInsights'] as const,
|
||||
lists: () => [...aiInsightsKeys.all, 'list'] as const,
|
||||
list: (tenantId: string, filters?: AIInsightFilters) => [...aiInsightsKeys.lists(), tenantId, filters] as const,
|
||||
details: () => [...aiInsightsKeys.all, 'detail'] as const,
|
||||
detail: (tenantId: string, insightId: string) => [...aiInsightsKeys.details(), tenantId, insightId] as const,
|
||||
stats: (tenantId: string, filters?: any) => [...aiInsightsKeys.all, 'stats', tenantId, filters] as const,
|
||||
orchestration: (tenantId: string, targetDate: string) => [...aiInsightsKeys.all, 'orchestration', tenantId, targetDate] as const,
|
||||
dashboard: (tenantId: string) => [...aiInsightsKeys.all, 'dashboard', tenantId] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get AI insights with filters
|
||||
*/
|
||||
export function useAIInsights(
|
||||
tenantId: string,
|
||||
filters?: AIInsightFilters,
|
||||
options?: Omit<UseQueryOptions<AIInsightListResponse>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: aiInsightsKeys.list(tenantId, filters),
|
||||
queryFn: () => aiInsightsService.getInsights(tenantId, filters),
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get a single AI insight
|
||||
*/
|
||||
export function useAIInsight(
|
||||
tenantId: string,
|
||||
insightId: string,
|
||||
options?: Omit<UseQueryOptions<AIInsight>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: aiInsightsKeys.detail(tenantId, insightId),
|
||||
queryFn: () => aiInsightsService.getInsight(tenantId, insightId),
|
||||
enabled: !!insightId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get AI insight statistics
|
||||
*/
|
||||
export function useAIInsightStats(
|
||||
tenantId: string,
|
||||
filters?: { start_date?: string; end_date?: string },
|
||||
options?: Omit<UseQueryOptions<AIInsightStatsResponse>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: aiInsightsKeys.stats(tenantId, filters),
|
||||
queryFn: () => aiInsightsService.getInsightStats(tenantId, filters),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get orchestration-ready insights
|
||||
*/
|
||||
export function useOrchestrationReadyInsights(
|
||||
tenantId: string,
|
||||
request: OrchestrationReadyInsightsRequest,
|
||||
options?: Omit<UseQueryOptions<OrchestrationReadyInsightsResponse>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: aiInsightsKeys.orchestration(tenantId, request.target_date),
|
||||
queryFn: () => aiInsightsService.getOrchestrationReadyInsights(tenantId, request),
|
||||
enabled: !!request.target_date,
|
||||
staleTime: 1000 * 60 * 10, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get dashboard summary
|
||||
*/
|
||||
export function useAIInsightsDashboard(
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<any>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: aiInsightsKeys.dashboard(tenantId),
|
||||
queryFn: () => aiInsightsService.getDashboardSummary(tenantId),
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get high priority insights
|
||||
*/
|
||||
export function useHighPriorityInsights(
|
||||
tenantId: string,
|
||||
limit: number = 10,
|
||||
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...aiInsightsKeys.lists(), tenantId, 'highPriority', limit],
|
||||
queryFn: () => aiInsightsService.getHighPriorityInsights(tenantId, limit),
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get actionable insights
|
||||
*/
|
||||
export function useActionableInsights(
|
||||
tenantId: string,
|
||||
limit: number = 20,
|
||||
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...aiInsightsKeys.lists(), tenantId, 'actionable', limit],
|
||||
queryFn: () => aiInsightsService.getActionableInsights(tenantId, limit),
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get insights by category
|
||||
*/
|
||||
export function useInsightsByCategory(
|
||||
tenantId: string,
|
||||
category: string,
|
||||
limit: number = 20,
|
||||
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...aiInsightsKeys.lists(), tenantId, 'category', category, limit],
|
||||
queryFn: () => aiInsightsService.getInsightsByCategory(tenantId, category, limit),
|
||||
enabled: !!category,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to search insights
|
||||
*/
|
||||
export function useSearchInsights(
|
||||
tenantId: string,
|
||||
query: string,
|
||||
filters?: Partial<AIInsightFilters>,
|
||||
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...aiInsightsKeys.lists(), tenantId, 'search', query, filters],
|
||||
queryFn: () => aiInsightsService.searchInsights(tenantId, query, filters),
|
||||
enabled: query.length > 0,
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation hook to apply an insight
|
||||
*/
|
||||
export function useApplyInsight(
|
||||
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string }>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, insightId }: { tenantId: string; insightId: string }) =>
|
||||
aiInsightsService.applyInsight(tenantId, insightId),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate all insight queries for this tenant
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.dashboard(variables.tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation hook to dismiss an insight
|
||||
*/
|
||||
export function useDismissInsight(
|
||||
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; reason?: string }>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, insightId, reason }) =>
|
||||
aiInsightsService.dismissInsight(tenantId, insightId, reason),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.dashboard(variables.tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation hook to resolve an insight
|
||||
*/
|
||||
export function useResolveInsight(
|
||||
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; resolution?: string }>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, insightId, resolution }) =>
|
||||
aiInsightsService.resolveInsight(tenantId, insightId, resolution),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.dashboard(variables.tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation hook to record feedback for an insight
|
||||
*/
|
||||
export function useRecordFeedback(
|
||||
options?: UseMutationOptions<any, Error, { tenantId: string; insightId: string; feedback: FeedbackRequest }>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, insightId, feedback }) =>
|
||||
aiInsightsService.recordFeedback(tenantId, insightId, feedback),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility hook to manage insight selection
|
||||
*/
|
||||
export function useInsightSelection() {
|
||||
const [selectedInsights, setSelectedInsights] = useState<string[]>([]);
|
||||
|
||||
const toggleInsight = (insightId: string) => {
|
||||
setSelectedInsights((prev) =>
|
||||
prev.includes(insightId)
|
||||
? prev.filter((id) => id !== insightId)
|
||||
: [...prev, insightId]
|
||||
);
|
||||
};
|
||||
|
||||
const selectAll = (insightIds: string[]) => {
|
||||
setSelectedInsights(insightIds);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedInsights([]);
|
||||
};
|
||||
|
||||
return {
|
||||
selectedInsights,
|
||||
toggleInsight,
|
||||
selectAll,
|
||||
clearSelection,
|
||||
isSelected: (insightId: string) => selectedInsights.includes(insightId),
|
||||
};
|
||||
}
|
||||
|
||||
// Import useState for utility hook
|
||||
import { useState } from 'react';
|
||||
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;
|
||||
|
||||
@@ -121,6 +121,31 @@ export interface SupplierSelectionSettings {
|
||||
enable_supplier_score_optimization: boolean;
|
||||
}
|
||||
|
||||
export interface MLInsightsSettings {
|
||||
// Inventory ML (Safety Stock Optimization)
|
||||
inventory_lookback_days: number;
|
||||
inventory_min_history_days: number;
|
||||
|
||||
// Production ML (Yield Prediction)
|
||||
production_lookback_days: number;
|
||||
production_min_history_runs: number;
|
||||
|
||||
// Procurement ML (Supplier Analysis & Price Forecasting)
|
||||
supplier_analysis_lookback_days: number;
|
||||
supplier_analysis_min_orders: number;
|
||||
price_forecast_lookback_days: number;
|
||||
price_forecast_horizon_days: number;
|
||||
|
||||
// Forecasting ML (Dynamic Rules)
|
||||
rules_generation_lookback_days: number;
|
||||
rules_generation_min_samples: number;
|
||||
|
||||
// Global ML Settings
|
||||
enable_ml_insights: boolean;
|
||||
ml_insights_auto_trigger: boolean;
|
||||
ml_confidence_threshold: number;
|
||||
}
|
||||
|
||||
export interface TenantSettings {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
@@ -134,6 +159,7 @@ export interface TenantSettings {
|
||||
safety_stock_settings: SafetyStockSettings;
|
||||
moq_settings: MOQSettings;
|
||||
supplier_selection_settings: SupplierSelectionSettings;
|
||||
ml_insights_settings: MLInsightsSettings;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -149,6 +175,7 @@ export interface TenantSettingsUpdate {
|
||||
safety_stock_settings?: Partial<SafetyStockSettings>;
|
||||
moq_settings?: Partial<MOQSettings>;
|
||||
supplier_selection_settings?: Partial<SupplierSelectionSettings>;
|
||||
ml_insights_settings?: Partial<MLInsightsSettings>;
|
||||
}
|
||||
|
||||
export type SettingsCategory =
|
||||
@@ -161,7 +188,8 @@ export type SettingsCategory =
|
||||
| 'replenishment'
|
||||
| 'safety_stock'
|
||||
| 'moq'
|
||||
| 'supplier_selection';
|
||||
| 'supplier_selection'
|
||||
| 'ml_insights';
|
||||
|
||||
export interface CategoryResetResponse {
|
||||
category: string;
|
||||
|
||||
@@ -9,6 +9,8 @@ interface ModelDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
model: TrainedModelResponse;
|
||||
onRetrain?: (settings: any) => void;
|
||||
onViewPredictions?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
// Helper function to determine performance color based on accuracy
|
||||
@@ -89,7 +91,9 @@ const FeatureTag: React.FC<{ feature: string }> = ({ feature }) => {
|
||||
const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
model
|
||||
model,
|
||||
onRetrain,
|
||||
onViewPredictions
|
||||
}) => {
|
||||
// Early return if model is not provided
|
||||
if (!model) {
|
||||
@@ -173,7 +177,9 @@ const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
|
||||
},
|
||||
{
|
||||
label: "Período de Entrenamiento",
|
||||
value: `${formatDate((model as any).training_start_date || model.training_period?.start_date || new Date().toISOString())} - ${formatDate((model as any).training_end_date || model.training_period?.end_date || new Date().toISOString())}`
|
||||
value: model.data_period_start && model.data_period_end
|
||||
? `${formatDate(model.data_period_start)} a ${formatDate(model.data_period_end)}`
|
||||
: 'Datos no disponibles'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -307,7 +313,9 @@ const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
|
||||
},
|
||||
{
|
||||
label: "Período de Entrenamiento",
|
||||
value: `${formatDate((model as any).training_start_date || model.training_period?.start_date || new Date().toISOString())} a ${formatDate((model as any).training_end_date || model.training_period?.end_date || new Date().toISOString())}`,
|
||||
value: model.data_period_start && model.data_period_end
|
||||
? `${formatDate(model.data_period_start)} a ${formatDate(model.data_period_end)}`
|
||||
: 'Datos no disponibles',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
@@ -360,16 +368,27 @@ const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
|
||||
label: 'Actualizar Modelo',
|
||||
variant: 'primary' as const,
|
||||
onClick: () => {
|
||||
// TODO: Implement model retraining functionality
|
||||
// This should trigger a new training job for the product
|
||||
// Implement model retraining functionality
|
||||
// This triggers a new training job for the product using the existing API
|
||||
if (onRetrain && model?.inventory_product_id) {
|
||||
onRetrain({
|
||||
seasonality_mode: 'additive',
|
||||
daily_seasonality: true,
|
||||
weekly_seasonality: true,
|
||||
yearly_seasonality: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Ver Predicciones',
|
||||
variant: 'secondary' as const,
|
||||
onClick: () => {
|
||||
// TODO: Navigate to forecast history or predictions view
|
||||
// This should show historical predictions vs actual sales
|
||||
// Navigate to forecast history or predictions view
|
||||
// This shows historical predictions vs actual sales
|
||||
if (onViewPredictions && model?.model_id) {
|
||||
onViewPredictions(model.model_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -401,4 +420,4 @@ const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelDetailsModal;
|
||||
export default ModelDetailsModal;
|
||||
|
||||
@@ -330,8 +330,8 @@ export const RetrainModelModal: React.FC<RetrainModelModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Define tab-style actions for header navigation - memoized
|
||||
const actions: EditViewModalAction[] = React.useMemo(() => [
|
||||
// Header navigation actions (tabs)
|
||||
const headerActions = React.useMemo(() => [
|
||||
{
|
||||
label: t('models:retrain.modes.quick', 'Rápido'),
|
||||
icon: Zap,
|
||||
@@ -358,8 +358,11 @@ export const RetrainModelModal: React.FC<RetrainModelModalProps> = ({
|
||||
return (
|
||||
<EditViewModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode="edit"
|
||||
onClose={() => {
|
||||
setMode('quick');
|
||||
onClose();
|
||||
}}
|
||||
mode="edit" // Keep in edit mode so it shows Cancel/Save
|
||||
title={t('models:retrain.title', 'Reentrenar Modelo')}
|
||||
subtitle={ingredient.name}
|
||||
statusIndicator={{
|
||||
@@ -371,9 +374,10 @@ export const RetrainModelModal: React.FC<RetrainModelModalProps> = ({
|
||||
}}
|
||||
size="lg"
|
||||
sections={sections}
|
||||
actions={actions}
|
||||
actions={headerActions}
|
||||
actionsPosition="header"
|
||||
showDefaultActions={true}
|
||||
showDefaultActions={true} // Enable default actions (Cancel/Save)
|
||||
saveLabel={t('models:retrain.start', 'Iniciar Reentrenamiento')} // Custom save button label for retraining
|
||||
onSave={handleRetrain}
|
||||
onFieldChange={handleFieldChange}
|
||||
loading={isLoading}
|
||||
|
||||
@@ -272,6 +272,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
|
||||
const ingredientData = {
|
||||
name: item.suggested_name,
|
||||
product_type: item.product_type,
|
||||
category: item.category,
|
||||
unit_of_measure: item.unit_of_measure,
|
||||
low_stock_threshold: minimumStock,
|
||||
|
||||
@@ -41,7 +41,14 @@ import {
|
||||
ChefHat,
|
||||
ClipboardCheck,
|
||||
BrainCircuit,
|
||||
Cog
|
||||
Cog,
|
||||
TrendingUp,
|
||||
Gauge,
|
||||
PlayCircle,
|
||||
Layers,
|
||||
Lightbulb,
|
||||
Activity,
|
||||
List
|
||||
} from 'lucide-react';
|
||||
|
||||
export interface SidebarProps {
|
||||
@@ -120,6 +127,13 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
'clipboard-check': ClipboardCheck,
|
||||
'brain-circuit': BrainCircuit,
|
||||
cog: Cog,
|
||||
analytics: TrendingUp,
|
||||
performance: Gauge,
|
||||
simulation: PlayCircle,
|
||||
scenarios: Layers,
|
||||
insights: Lightbulb,
|
||||
events: Activity,
|
||||
list: List,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -182,10 +196,14 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
'/app/database': 'navigation.data',
|
||||
'/app/database/inventory': 'navigation.inventory',
|
||||
'/app/analytics': 'navigation.analytics',
|
||||
'/app/analytics/production': 'navigation.production_analytics',
|
||||
'/app/analytics/procurement': 'navigation.procurement_analytics',
|
||||
'/app/analytics/forecasting': 'navigation.forecasting',
|
||||
'/app/analytics/scenario-simulation': 'navigation.scenario_simulation',
|
||||
'/app/analytics/sales': 'navigation.sales',
|
||||
'/app/analytics/performance': 'navigation.performance',
|
||||
'/app/analytics/sales': 'navigation.sales_analytics',
|
||||
'/app/analytics/performance': 'navigation.performance_kpis',
|
||||
'/app/analytics/ai-insights': 'navigation.ai_insights',
|
||||
'/app/analytics/events': 'navigation.system_events',
|
||||
'/app/ai': 'navigation.insights',
|
||||
'/app/communications': 'navigation.communications',
|
||||
'/app/communications/notifications': 'navigation.notifications',
|
||||
|
||||
@@ -82,6 +82,11 @@ export interface EditViewModalProps {
|
||||
isRefetching?: boolean; // External refetch state (from React Query)
|
||||
onSaveComplete?: () => Promise<void>; // Async callback for triggering refetch
|
||||
refetchTimeout?: number; // Timeout in ms for refetch (default: 3000)
|
||||
|
||||
// Custom default action labels
|
||||
cancelLabel?: string; // Custom label for cancel button
|
||||
saveLabel?: string; // Custom label for save button
|
||||
editLabel?: string; // Custom label for edit button
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,6 +356,10 @@ export const EditViewModal: React.FC<EditViewModalProps> = ({
|
||||
isRefetching = false,
|
||||
onSaveComplete,
|
||||
refetchTimeout = 3000,
|
||||
// Custom default action labels
|
||||
cancelLabel,
|
||||
saveLabel,
|
||||
editLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation(['common']);
|
||||
const StatusIcon = statusIndicator?.icon;
|
||||
@@ -449,13 +458,13 @@ export const EditViewModal: React.FC<EditViewModalProps> = ({
|
||||
if (mode === 'view') {
|
||||
defaultActions.push(
|
||||
{
|
||||
label: t('common:modals.actions.cancel', 'Cancelar'),
|
||||
label: cancelLabel || t('common:modals.actions.cancel', 'Cancelar'),
|
||||
variant: 'outline',
|
||||
onClick: onClose,
|
||||
disabled: isProcessing,
|
||||
},
|
||||
{
|
||||
label: t('common:modals.actions.edit', 'Editar'),
|
||||
label: editLabel || t('common:modals.actions.edit', 'Editar'),
|
||||
variant: 'primary',
|
||||
onClick: handleEdit,
|
||||
disabled: isProcessing,
|
||||
@@ -464,13 +473,13 @@ export const EditViewModal: React.FC<EditViewModalProps> = ({
|
||||
} else {
|
||||
defaultActions.push(
|
||||
{
|
||||
label: t('common:modals.actions.cancel', 'Cancelar'),
|
||||
label: cancelLabel || t('common:modals.actions.cancel', 'Cancelar'),
|
||||
variant: 'outline',
|
||||
onClick: handleCancel,
|
||||
disabled: isProcessing,
|
||||
},
|
||||
{
|
||||
label: t('common:modals.actions.save', 'Guardar'),
|
||||
label: saveLabel || t('common:modals.actions.save', 'Guardar'),
|
||||
variant: 'primary',
|
||||
onClick: handleSave,
|
||||
disabled: isProcessing,
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
"procurement": "Procurement",
|
||||
"pos": "Point of Sale",
|
||||
"analytics": "Analytics",
|
||||
"production_analytics": "Production Dashboard",
|
||||
"procurement_analytics": "Procurement Dashboard",
|
||||
"sales_analytics": "Sales Dashboard",
|
||||
"performance_kpis": "General KPIs",
|
||||
"ai_insights": "Recommendations",
|
||||
"system_events": "System Events",
|
||||
"forecasting": "Forecasting",
|
||||
"scenario_simulation": "Scenario Simulation",
|
||||
"scenario_simulation": "What-If Analysis",
|
||||
"sales": "Sales",
|
||||
"performance": "Performance",
|
||||
"insights": "AI Insights",
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
"procurement": "Compras",
|
||||
"pos": "Punto de Venta",
|
||||
"analytics": "Análisis",
|
||||
"production_analytics": "Dashboard de Producción",
|
||||
"procurement_analytics": "Dashboard de Compras",
|
||||
"sales_analytics": "Dashboard de Ventas",
|
||||
"performance_kpis": "KPIs Generales",
|
||||
"ai_insights": "Recomendaciones",
|
||||
"system_events": "Eventos del Sistema",
|
||||
"forecasting": "Predicción",
|
||||
"scenario_simulation": "Simulación de Escenarios",
|
||||
"scenario_simulation": "Análisis What-If",
|
||||
"sales": "Ventas",
|
||||
"performance": "Rendimiento",
|
||||
"insights": "Insights IA",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"retrain": {
|
||||
"title": "Reentrenar Modelo",
|
||||
"subtitle": "Actualiza el modelo de predicción con datos recientes",
|
||||
"start": "Iniciar Reentrenamiento",
|
||||
|
||||
"modes": {
|
||||
"quick": "Rápido",
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
"procurement": "Erosketak",
|
||||
"pos": "Salmenta-puntua",
|
||||
"analytics": "Analisiak",
|
||||
"production_analytics": "Ekoizpen Aginte-panela",
|
||||
"procurement_analytics": "Erosketa Aginte-panela",
|
||||
"sales_analytics": "Salmenta Aginte-panela",
|
||||
"performance_kpis": "KPI Orokorra",
|
||||
"ai_insights": "Gomendioak",
|
||||
"system_events": "Sistema Gertaerak",
|
||||
"forecasting": "Aurreikuspenak",
|
||||
"scenario_simulation": "Agertoki-simulazioa",
|
||||
"scenario_simulation": "Zer-Baitezak Analisia",
|
||||
"sales": "Salmentak",
|
||||
"performance": "Errendimendua",
|
||||
"insights": "AA ikuspegiak",
|
||||
|
||||
@@ -2,115 +2,60 @@ import React, { useState } from 'react';
|
||||
import { Brain, TrendingUp, AlertTriangle, Lightbulb, Target, Zap, Download, RefreshCw } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useAIInsights, useAIInsightStats, useApplyInsight, useDismissInsight } from '../../../../api/hooks/aiInsights';
|
||||
import { AIInsight } from '../../../../api/services/aiInsights';
|
||||
|
||||
const AIInsightsPage: React.FC = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const currentTenant = useCurrentTenant();
|
||||
const user = useAuthUser();
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
const insights = [
|
||||
// Fetch real insights from API
|
||||
const { data: insightsData, isLoading, refetch } = useAIInsights(
|
||||
tenantId || '',
|
||||
{
|
||||
id: '1',
|
||||
type: 'optimization',
|
||||
priority: 'high',
|
||||
title: 'Optimización de Producción de Croissants',
|
||||
description: 'La demanda de croissants aumenta un 23% los viernes. Recomendamos incrementar la producción en 15 unidades.',
|
||||
impact: 'Aumento estimado de ingresos: €180/semana',
|
||||
confidence: 87,
|
||||
category: 'production',
|
||||
timestamp: '2024-01-26 09:30',
|
||||
actionable: true,
|
||||
metrics: {
|
||||
currentProduction: 45,
|
||||
recommendedProduction: 60,
|
||||
expectedIncrease: '+23%'
|
||||
}
|
||||
status: 'active',
|
||||
category: selectedCategory === 'all' ? undefined : selectedCategory,
|
||||
limit: 100,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'alert',
|
||||
priority: 'medium',
|
||||
title: 'Patrón de Compra en Tardes',
|
||||
description: 'Los clientes compran más productos salados después de las 16:00. Considera promocionar empanadas durante estas horas.',
|
||||
impact: 'Potencial aumento de ventas: 12%',
|
||||
confidence: 92,
|
||||
category: 'sales',
|
||||
timestamp: '2024-01-26 08:45',
|
||||
actionable: true,
|
||||
metrics: {
|
||||
afternoonSales: '+15%',
|
||||
savoryProducts: '68%',
|
||||
conversionRate: '12.3%'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'prediction',
|
||||
priority: 'high',
|
||||
title: 'Predicción de Demanda de San Valentín',
|
||||
description: 'Se espera un incremento del 40% en la demanda de productos de repostería especiales entre el 10-14 de febrero.',
|
||||
impact: 'Preparar stock adicional de ingredientes premium',
|
||||
confidence: 94,
|
||||
category: 'forecasting',
|
||||
timestamp: '2024-01-26 07:15',
|
||||
actionable: true,
|
||||
metrics: {
|
||||
expectedIncrease: '+40%',
|
||||
daysAhead: 18,
|
||||
recommendedPrep: '3 días'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'recommendation',
|
||||
priority: 'low',
|
||||
title: 'Optimización de Inventario de Harina',
|
||||
description: 'El consumo de harina integral ha disminuido 8% este mes. Considera ajustar las órdenes de compra.',
|
||||
impact: 'Reducción de desperdicios: €45/mes',
|
||||
confidence: 78,
|
||||
category: 'inventory',
|
||||
timestamp: '2024-01-25 16:20',
|
||||
actionable: false,
|
||||
metrics: {
|
||||
consumption: '-8%',
|
||||
currentStock: '45kg',
|
||||
recommendedOrder: '25kg'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'insight',
|
||||
priority: 'medium',
|
||||
title: 'Análisis de Satisfacción del Cliente',
|
||||
description: 'Los clientes valoran más la frescura (95%) que el precio (67%). Enfoque en destacar la calidad artesanal.',
|
||||
impact: 'Mejorar estrategia de marketing',
|
||||
confidence: 89,
|
||||
category: 'customer',
|
||||
timestamp: '2024-01-25 14:30',
|
||||
actionable: true,
|
||||
metrics: {
|
||||
freshnessScore: '95%',
|
||||
priceScore: '67%',
|
||||
qualityScore: '91%'
|
||||
}
|
||||
}
|
||||
];
|
||||
{ enabled: !!tenantId }
|
||||
);
|
||||
|
||||
// Fetch stats
|
||||
const { data: stats } = useAIInsightStats(
|
||||
tenantId || '',
|
||||
{},
|
||||
{ enabled: !!tenantId }
|
||||
);
|
||||
|
||||
// Mutations
|
||||
const applyMutation = useApplyInsight();
|
||||
const dismissMutation = useDismissInsight();
|
||||
|
||||
const insights: AIInsight[] = insightsData?.items || [];
|
||||
|
||||
// Use real insights data
|
||||
const displayInsights = insights;
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todas las Categorías', count: insights.length },
|
||||
{ value: 'production', label: 'Producción', count: insights.filter(i => i.category === 'production').length },
|
||||
{ value: 'sales', label: 'Ventas', count: insights.filter(i => i.category === 'sales').length },
|
||||
{ value: 'forecasting', label: 'Pronósticos', count: insights.filter(i => i.category === 'forecasting').length },
|
||||
{ value: 'inventory', label: 'Inventario', count: insights.filter(i => i.category === 'inventory').length },
|
||||
{ value: 'customer', label: 'Clientes', count: insights.filter(i => i.category === 'customer').length },
|
||||
{ value: 'all', label: 'Todas las Categorías', count: stats?.total_insights || 0 },
|
||||
{ value: 'production', label: 'Producción', count: stats?.insights_by_category?.production || 0 },
|
||||
{ value: 'sales', label: 'Ventas', count: stats?.insights_by_category?.sales || 0 },
|
||||
{ value: 'demand', label: 'Pronósticos', count: stats?.insights_by_category?.demand || 0 },
|
||||
{ value: 'inventory', label: 'Inventario', count: stats?.insights_by_category?.inventory || 0 },
|
||||
{ value: 'procurement', label: 'Compras', count: stats?.insights_by_category?.procurement || 0 },
|
||||
];
|
||||
|
||||
const aiMetrics = {
|
||||
totalInsights: insights.length,
|
||||
actionableInsights: insights.filter(i => i.actionable).length,
|
||||
averageConfidence: Math.round(insights.reduce((sum, i) => sum + i.confidence, 0) / insights.length),
|
||||
highPriorityInsights: insights.filter(i => i.priority === 'high').length,
|
||||
mediumPriorityInsights: insights.filter(i => i.priority === 'medium').length,
|
||||
lowPriorityInsights: insights.filter(i => i.priority === 'low').length,
|
||||
totalInsights: stats?.total_insights || 0,
|
||||
actionableInsights: stats?.actionable_insights || 0,
|
||||
averageConfidence: stats?.avg_confidence ? Math.round(stats.avg_confidence) : 0,
|
||||
highPriorityInsights: stats?.insights_by_priority?.high || stats?.insights_by_priority?.urgent || 0,
|
||||
mediumPriorityInsights: stats?.insights_by_priority?.medium || 0,
|
||||
lowPriorityInsights: stats?.insights_by_priority?.low || 0,
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
@@ -145,14 +90,32 @@ const AIInsightsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredInsights = selectedCategory === 'all'
|
||||
? insights
|
||||
: insights.filter(insight => insight.category === selectedCategory);
|
||||
const filteredInsights = selectedCategory === 'all'
|
||||
? displayInsights
|
||||
: displayInsights.filter(insight => insight.category === selectedCategory);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setIsRefreshing(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
const handleApplyInsight = async (insightId: string) => {
|
||||
if (!tenantId) return;
|
||||
try {
|
||||
await applyMutation.mutateAsync({ tenantId, insightId });
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
console.error('Failed to apply insight:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismissInsight = async (insightId: string) => {
|
||||
if (!tenantId) return;
|
||||
try {
|
||||
await dismissMutation.mutateAsync({ tenantId, insightId });
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
console.error('Failed to dismiss insight:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -161,7 +124,7 @@ const AIInsightsPage: React.FC = () => {
|
||||
description="Insights inteligentes y recomendaciones automáticas para optimizar tu panadería"
|
||||
subscriptionLoading={false}
|
||||
hasAccess={true}
|
||||
dataLoading={isRefreshing}
|
||||
dataLoading={isLoading || applyMutation.isLoading || dismissMutation.isLoading}
|
||||
actions={[
|
||||
{
|
||||
id: 'refresh',
|
||||
@@ -169,7 +132,7 @@ const AIInsightsPage: React.FC = () => {
|
||||
icon: RefreshCw,
|
||||
onClick: handleRefresh,
|
||||
variant: 'outline',
|
||||
disabled: isRefreshing,
|
||||
disabled: isLoading,
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
@@ -279,9 +242,23 @@ const AIInsightsPage: React.FC = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-[var(--text-tertiary)]">{insight.timestamp}</p>
|
||||
{insight.actionable && (
|
||||
<Button size="sm">
|
||||
Aplicar Recomendación
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleApplyInsight(insight.id)}
|
||||
disabled={applyMutation.isLoading}
|
||||
>
|
||||
Aplicar Recomendación
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDismissInsight(insight.id)}
|
||||
disabled={dismissMutation.isLoading}
|
||||
>
|
||||
Descartar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -370,24 +370,38 @@ const ModelsConfigPage: React.FC = () => {
|
||||
handleStartTraining(status.ingredient);
|
||||
}
|
||||
}}
|
||||
actions={[
|
||||
// Primary action - View details or train model
|
||||
{
|
||||
label: status.hasModel ? 'Ver Detalles' : 'Entrenar',
|
||||
icon: status.hasModel ? Eye : Play,
|
||||
onClick: () => status.hasModel
|
||||
? handleViewModelDetails(status.ingredient)
|
||||
: handleStartTraining(status.ingredient),
|
||||
priority: 'primary' as const
|
||||
},
|
||||
// Secondary action - Retrain if model exists
|
||||
...(status.hasModel ? [{
|
||||
label: 'Reentrenar',
|
||||
icon: RotateCcw,
|
||||
onClick: () => handleStartRetraining(status.ingredient),
|
||||
priority: 'secondary' as const
|
||||
}] : [])
|
||||
]}
|
||||
actions={
|
||||
(() => {
|
||||
if (status.hasModel) {
|
||||
// For models that exist: prioritize retraining action as primary (text button)
|
||||
// and details as secondary (icon button)
|
||||
return [
|
||||
{
|
||||
label: 'Reentrenar',
|
||||
icon: RotateCcw,
|
||||
onClick: () => handleStartRetraining(status.ingredient),
|
||||
priority: 'primary' as const
|
||||
},
|
||||
{
|
||||
label: 'Ver Detalles',
|
||||
icon: Eye,
|
||||
onClick: () => handleViewModelDetails(status.ingredient),
|
||||
priority: 'secondary' as const
|
||||
}
|
||||
];
|
||||
} else {
|
||||
// For models that don't exist: only train action
|
||||
return [
|
||||
{
|
||||
label: 'Entrenar',
|
||||
icon: Play,
|
||||
onClick: () => handleStartTraining(status.ingredient),
|
||||
priority: 'primary' as const
|
||||
}
|
||||
];
|
||||
}
|
||||
})()
|
||||
}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -479,6 +493,12 @@ const ModelsConfigPage: React.FC = () => {
|
||||
isOpen={showModelDetailsModal}
|
||||
onClose={() => setShowModelDetailsModal(false)}
|
||||
model={selectedModel}
|
||||
onRetrain={handleRetrain}
|
||||
onViewPredictions={(modelId) => {
|
||||
// TODO: Navigate to forecast history or predictions view
|
||||
// This should show historical predictions vs actual sales
|
||||
console.log('View predictions for model:', modelId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
name: 'Analytics',
|
||||
component: 'AnalyticsPage',
|
||||
title: 'Análisis',
|
||||
icon: 'sales',
|
||||
icon: 'analytics',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
requiredAnalyticsLevel: 'basic',
|
||||
@@ -288,7 +288,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/production',
|
||||
name: 'ProductionAnalytics',
|
||||
component: 'ProductionAnalyticsPage',
|
||||
title: 'Análisis de Producción',
|
||||
title: 'Dashboard de Producción',
|
||||
icon: 'production',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
@@ -300,7 +300,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/procurement',
|
||||
name: 'ProcurementAnalytics',
|
||||
component: 'ProcurementAnalyticsPage',
|
||||
title: 'Análisis de Compras',
|
||||
title: 'Dashboard de Compras',
|
||||
icon: 'procurement',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
@@ -312,7 +312,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/forecasting',
|
||||
name: 'Forecasting',
|
||||
component: 'ForecastingPage',
|
||||
title: 'Pronósticos',
|
||||
title: 'Predicciones',
|
||||
icon: 'forecasting',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
@@ -324,7 +324,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/sales',
|
||||
name: 'SalesAnalytics',
|
||||
component: 'SalesAnalyticsPage',
|
||||
title: 'Análisis de Ventas',
|
||||
title: 'Dashboard de Ventas',
|
||||
icon: 'sales',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
@@ -336,8 +336,8 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/performance',
|
||||
name: 'PerformanceAnalytics',
|
||||
component: 'PerformanceAnalyticsPage',
|
||||
title: 'Análisis de Rendimiento',
|
||||
icon: 'sales',
|
||||
title: 'KPIs Generales',
|
||||
icon: 'performance',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
requiredAnalyticsLevel: 'advanced',
|
||||
@@ -348,8 +348,8 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/scenario-simulation',
|
||||
name: 'ScenarioSimulation',
|
||||
component: 'ScenarioSimulationPage',
|
||||
title: 'Simulación de Escenarios',
|
||||
icon: 'forecasting',
|
||||
title: 'Análisis What-If',
|
||||
icon: 'simulation',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
requiredAnalyticsLevel: 'advanced',
|
||||
@@ -360,8 +360,8 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/ai-insights',
|
||||
name: 'AIInsights',
|
||||
component: 'AIInsightsPage',
|
||||
title: 'Insights de IA',
|
||||
icon: 'forecasting',
|
||||
title: 'Recomendaciones',
|
||||
icon: 'insights',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
requiredAnalyticsLevel: 'predictive',
|
||||
@@ -372,8 +372,8 @@ export const routesConfig: RouteConfig[] = [
|
||||
path: '/app/analytics/events',
|
||||
name: 'EventRegistry',
|
||||
component: 'EventRegistryPage',
|
||||
title: 'Registro de Eventos',
|
||||
icon: 'fileText',
|
||||
title: 'Eventos del Sistema',
|
||||
icon: 'events',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'owner'],
|
||||
showInNavigation: true,
|
||||
|
||||
Reference in New Issue
Block a user