Add AI insights feature
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
aiInsightsService,
|
||||
AIInsight,
|
||||
@@ -213,13 +214,13 @@ export function useApplyInsight(
|
||||
* Mutation hook to dismiss an insight
|
||||
*/
|
||||
export function useDismissInsight(
|
||||
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; reason?: string }>
|
||||
options?: UseMutationOptions<void, Error, { tenantId: string; insightId: string }>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, insightId, reason }) =>
|
||||
aiInsightsService.dismissInsight(tenantId, insightId, reason),
|
||||
mutationFn: ({ tenantId, insightId }) =>
|
||||
aiInsightsService.dismissInsight(tenantId, insightId),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
|
||||
@@ -231,16 +232,16 @@ export function useDismissInsight(
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation hook to resolve an insight
|
||||
* Mutation hook to update insight status
|
||||
*/
|
||||
export function useResolveInsight(
|
||||
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; resolution?: string }>
|
||||
export function useUpdateInsightStatus(
|
||||
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; status: 'acknowledged' | 'in_progress' | 'applied' | 'expired' }>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, insightId, resolution }) =>
|
||||
aiInsightsService.resolveInsight(tenantId, insightId, resolution),
|
||||
mutationFn: ({ tenantId, insightId, status }) =>
|
||||
aiInsightsService.updateInsightStatus(tenantId, insightId, status),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
|
||||
@@ -300,6 +301,3 @@ export function useInsightSelection() {
|
||||
isSelected: (insightId: string) => selectedInsights.includes(insightId),
|
||||
};
|
||||
}
|
||||
|
||||
// Import useState for utility hook
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -13,10 +13,41 @@ import { productionService } from '../services/production';
|
||||
import { ProcurementService } from '../services/procurement-service';
|
||||
import * as orchestratorService from '../services/orchestrator';
|
||||
import { suppliersService } from '../services/suppliers';
|
||||
import { aiInsightsService } from '../services/aiInsights';
|
||||
import { useBatchNotifications, useDeliveryNotifications, useOrchestrationNotifications } from '../../hooks/useEventNotifications';
|
||||
import { useSSEEvents } from '../../hooks/useSSE';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
||||
// ============================================================
|
||||
// Helper Functions
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Map AI insight category to dashboard block type
|
||||
*/
|
||||
function mapInsightTypeToBlockType(category: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'inventory': 'safety_stock',
|
||||
'forecasting': 'demand_forecast',
|
||||
'demand': 'demand_forecast',
|
||||
'procurement': 'cost_optimization',
|
||||
'cost': 'cost_optimization',
|
||||
'production': 'waste_reduction',
|
||||
'quality': 'risk_alert',
|
||||
'efficiency': 'waste_reduction',
|
||||
};
|
||||
return mapping[category] || 'demand_forecast';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map AI insight priority to dashboard impact level
|
||||
*/
|
||||
function mapPriorityToImpact(priority: string): 'high' | 'medium' | 'low' {
|
||||
if (priority === 'critical' || priority === 'high') return 'high';
|
||||
if (priority === 'medium') return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
@@ -75,20 +106,26 @@ export function useDashboardData(tenantId: string) {
|
||||
const now = new Date(); // Keep for local time display
|
||||
const nowUTC = new Date(); // UTC time for accurate comparison with API dates
|
||||
|
||||
// Parallel fetch ALL data needed by all 4 blocks (including suppliers for PO enrichment)
|
||||
const [alertsResponse, pendingPOs, productionResponse, deliveriesResponse, orchestration, suppliers] = await Promise.all([
|
||||
// Parallel fetch ALL data needed by all 4 blocks (including suppliers for PO enrichment and AI insights)
|
||||
const [alertsResponse, pendingPOs, productionResponse, deliveriesResponse, orchestration, suppliers, aiInsightsResponse] = await Promise.all([
|
||||
alertService.getEvents(tenantId, { status: 'active', limit: 100 }).catch(() => []),
|
||||
getPendingApprovalPurchaseOrders(tenantId, 100).catch(() => []),
|
||||
productionService.getBatches(tenantId, { start_date: today, page_size: 100 }).catch(() => ({ batches: [] })),
|
||||
ProcurementService.getExpectedDeliveries(tenantId, { days_ahead: 1, include_overdue: true }).catch(() => ({ deliveries: [] })),
|
||||
orchestratorService.getLastOrchestrationRun(tenantId).catch(() => null),
|
||||
suppliersService.getSuppliers(tenantId).catch(() => []),
|
||||
aiInsightsService.getInsights(tenantId, {
|
||||
status: 'new',
|
||||
priority: 'high',
|
||||
limit: 5
|
||||
}).catch(() => ({ items: [], total: 0, limit: 5, offset: 0, has_more: false })),
|
||||
]);
|
||||
|
||||
// Normalize alerts (API returns array directly or {items: []})
|
||||
const alerts = Array.isArray(alertsResponse) ? alertsResponse : (alertsResponse?.items || []);
|
||||
const productionBatches = productionResponse?.batches || [];
|
||||
const deliveries = deliveriesResponse?.deliveries || [];
|
||||
const aiInsights = aiInsightsResponse?.items || [];
|
||||
|
||||
// Create supplier ID -> supplier name map for quick lookup
|
||||
const supplierMap = new Map<string, string>();
|
||||
@@ -246,6 +283,19 @@ export function useDashboardData(tenantId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
// Map AI insights to dashboard format
|
||||
const mappedAiInsights = aiInsights.map((insight: any) => ({
|
||||
id: insight.id,
|
||||
title: insight.title,
|
||||
description: insight.description,
|
||||
type: mapInsightTypeToBlockType(insight.category),
|
||||
impact: mapPriorityToImpact(insight.priority),
|
||||
impact_value: insight.impact_value?.toString(),
|
||||
impact_currency: insight.impact_unit === 'euros' ? '€' : '',
|
||||
created_at: insight.created_at,
|
||||
recommendation_actions: insight.recommendation_actions || [],
|
||||
}));
|
||||
|
||||
return {
|
||||
// Raw data
|
||||
alerts: deduplicatedAlerts,
|
||||
@@ -253,7 +303,7 @@ export function useDashboardData(tenantId: string) {
|
||||
productionBatches,
|
||||
deliveries,
|
||||
orchestrationSummary,
|
||||
aiInsights: [], // AI-generated insights for professional/enterprise tiers
|
||||
aiInsights: mappedAiInsights,
|
||||
|
||||
// Computed
|
||||
preventedIssues,
|
||||
@@ -295,6 +345,7 @@ export function useDashboardRealtimeSync(tenantId: string) {
|
||||
const { notifications: deliveryNotifications } = useDeliveryNotifications();
|
||||
const { recentNotifications: orchestrationNotifications } = useOrchestrationNotifications();
|
||||
const { events: alertEvents } = useSSEEvents({ channels: ['*.alerts'] });
|
||||
const { events: aiInsightEvents } = useSSEEvents({ channels: ['*.ai_insights'] });
|
||||
|
||||
// Invalidate dashboard data on batch events
|
||||
useEffect(() => {
|
||||
@@ -345,4 +396,15 @@ export function useDashboardRealtimeSync(tenantId: string) {
|
||||
refetchType: 'active',
|
||||
});
|
||||
}, [alertEvents, tenantId, queryClient]);
|
||||
|
||||
// Invalidate dashboard data on AI insight events
|
||||
useEffect(() => {
|
||||
if (!aiInsightEvents || aiInsightEvents.length === 0 || !tenantId) return;
|
||||
|
||||
// Any new AI insight should trigger a refresh
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['dashboard-data', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
}, [aiInsightEvents, tenantId, queryClient]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user