New alert service
This commit is contained in:
@@ -1,545 +0,0 @@
|
||||
/**
|
||||
* Alert Processor React Query hooks
|
||||
* Provides data fetching, caching, and state management for alert processing operations
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { alertProcessorService } from '../services/alert_processor';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
import type {
|
||||
AlertResponse,
|
||||
AlertUpdateRequest,
|
||||
AlertQueryParams,
|
||||
AlertDashboardData,
|
||||
NotificationSettings,
|
||||
ChannelRoutingConfig,
|
||||
WebhookConfig,
|
||||
AlertProcessingStatus,
|
||||
ProcessingMetrics,
|
||||
PaginatedResponse,
|
||||
} from '../types/alert_processor';
|
||||
|
||||
// Query Keys Factory
|
||||
export const alertProcessorKeys = {
|
||||
all: ['alert-processor'] as const,
|
||||
alerts: {
|
||||
all: () => [...alertProcessorKeys.all, 'alerts'] as const,
|
||||
lists: () => [...alertProcessorKeys.alerts.all(), 'list'] as const,
|
||||
list: (tenantId: string, params?: AlertQueryParams) =>
|
||||
[...alertProcessorKeys.alerts.lists(), tenantId, params] as const,
|
||||
details: () => [...alertProcessorKeys.alerts.all(), 'detail'] as const,
|
||||
detail: (tenantId: string, alertId: string) =>
|
||||
[...alertProcessorKeys.alerts.details(), tenantId, alertId] as const,
|
||||
dashboard: (tenantId: string) =>
|
||||
[...alertProcessorKeys.alerts.all(), 'dashboard', tenantId] as const,
|
||||
processingStatus: (tenantId: string, alertId: string) =>
|
||||
[...alertProcessorKeys.alerts.all(), 'processing-status', tenantId, alertId] as const,
|
||||
},
|
||||
notifications: {
|
||||
all: () => [...alertProcessorKeys.all, 'notifications'] as const,
|
||||
settings: (tenantId: string) =>
|
||||
[...alertProcessorKeys.notifications.all(), 'settings', tenantId] as const,
|
||||
routing: () =>
|
||||
[...alertProcessorKeys.notifications.all(), 'routing-config'] as const,
|
||||
},
|
||||
webhooks: {
|
||||
all: () => [...alertProcessorKeys.all, 'webhooks'] as const,
|
||||
list: (tenantId: string) =>
|
||||
[...alertProcessorKeys.webhooks.all(), tenantId] as const,
|
||||
},
|
||||
metrics: {
|
||||
all: () => [...alertProcessorKeys.all, 'metrics'] as const,
|
||||
processing: (tenantId: string) =>
|
||||
[...alertProcessorKeys.metrics.all(), 'processing', tenantId] as const,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Alert Queries
|
||||
export const useAlerts = (
|
||||
tenantId: string,
|
||||
queryParams?: AlertQueryParams,
|
||||
options?: Omit<UseQueryOptions<PaginatedResponse<AlertResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedResponse<AlertResponse>, ApiError>({
|
||||
queryKey: alertProcessorKeys.alerts.list(tenantId, queryParams),
|
||||
queryFn: () => alertProcessorService.getAlerts(tenantId, queryParams),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAlert = (
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
options?: Omit<UseQueryOptions<AlertResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<AlertResponse, ApiError>({
|
||||
queryKey: alertProcessorKeys.alerts.detail(tenantId, alertId),
|
||||
queryFn: () => alertProcessorService.getAlert(tenantId, alertId),
|
||||
enabled: !!tenantId && !!alertId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAlertDashboardData = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<AlertDashboardData, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<AlertDashboardData, ApiError>({
|
||||
queryKey: alertProcessorKeys.alerts.dashboard(tenantId),
|
||||
queryFn: () => alertProcessorService.getDashboardData(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
refetchInterval: 1 * 60 * 1000, // Refresh every minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAlertProcessingStatus = (
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
options?: Omit<UseQueryOptions<AlertProcessingStatus, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<AlertProcessingStatus, ApiError>({
|
||||
queryKey: alertProcessorKeys.alerts.processingStatus(tenantId, alertId),
|
||||
queryFn: () => alertProcessorService.getProcessingStatus(tenantId, alertId),
|
||||
enabled: !!tenantId && !!alertId,
|
||||
staleTime: 10 * 1000, // 10 seconds
|
||||
refetchInterval: 30 * 1000, // Poll every 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Notification Queries
|
||||
export const useNotificationSettings = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<NotificationSettings, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<NotificationSettings, ApiError>({
|
||||
queryKey: alertProcessorKeys.notifications.settings(tenantId),
|
||||
queryFn: () => alertProcessorService.getNotificationSettings(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useChannelRoutingConfig = (
|
||||
options?: Omit<UseQueryOptions<ChannelRoutingConfig, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ChannelRoutingConfig, ApiError>({
|
||||
queryKey: alertProcessorKeys.notifications.routing(),
|
||||
queryFn: () => alertProcessorService.getChannelRoutingConfig(),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Webhook Queries
|
||||
export const useWebhooks = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<WebhookConfig[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<WebhookConfig[], ApiError>({
|
||||
queryKey: alertProcessorKeys.webhooks.list(tenantId),
|
||||
queryFn: () => alertProcessorService.getWebhooks(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Metrics Queries
|
||||
export const useProcessingMetrics = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcessingMetrics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcessingMetrics, ApiError>({
|
||||
queryKey: alertProcessorKeys.metrics.processing(tenantId),
|
||||
queryFn: () => alertProcessorService.getProcessingMetrics(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Alert Mutations
|
||||
export const useUpdateAlert = (
|
||||
options?: UseMutationOptions<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; updateData: AlertUpdateRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; updateData: AlertUpdateRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, alertId, updateData }) =>
|
||||
alertProcessorService.updateAlert(tenantId, alertId, updateData),
|
||||
onSuccess: (data, { tenantId, alertId }) => {
|
||||
// Update the alert in cache
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.alerts.detail(tenantId, alertId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate alerts list to reflect the change
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.lists()
|
||||
});
|
||||
|
||||
// Invalidate dashboard data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.dashboard(tenantId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDismissAlert = (
|
||||
options?: UseMutationOptions<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, alertId }) =>
|
||||
alertProcessorService.dismissAlert(tenantId, alertId),
|
||||
onSuccess: (data, { tenantId, alertId }) => {
|
||||
// Update the alert in cache
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.alerts.detail(tenantId, alertId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.lists()
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.dashboard(tenantId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAcknowledgeAlert = (
|
||||
options?: UseMutationOptions<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; notes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; notes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, alertId, notes }) =>
|
||||
alertProcessorService.acknowledgeAlert(tenantId, alertId, notes),
|
||||
onSuccess: (data, { tenantId, alertId }) => {
|
||||
// Update the alert in cache
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.alerts.detail(tenantId, alertId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.lists()
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.dashboard(tenantId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useResolveAlert = (
|
||||
options?: UseMutationOptions<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; notes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
AlertResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; alertId: string; notes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, alertId, notes }) =>
|
||||
alertProcessorService.resolveAlert(tenantId, alertId, notes),
|
||||
onSuccess: (data, { tenantId, alertId }) => {
|
||||
// Update the alert in cache
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.alerts.detail(tenantId, alertId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.lists()
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.dashboard(tenantId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Notification Settings Mutations
|
||||
export const useUpdateNotificationSettings = (
|
||||
options?: UseMutationOptions<
|
||||
NotificationSettings,
|
||||
ApiError,
|
||||
{ tenantId: string; settings: Partial<NotificationSettings> }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
NotificationSettings,
|
||||
ApiError,
|
||||
{ tenantId: string; settings: Partial<NotificationSettings> }
|
||||
>({
|
||||
mutationFn: ({ tenantId, settings }) =>
|
||||
alertProcessorService.updateNotificationSettings(tenantId, settings),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Update settings in cache
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.notifications.settings(tenantId),
|
||||
data
|
||||
);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Webhook Mutations
|
||||
export const useCreateWebhook = (
|
||||
options?: UseMutationOptions<
|
||||
WebhookConfig,
|
||||
ApiError,
|
||||
{ tenantId: string; webhook: Omit<WebhookConfig, 'tenant_id'> }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
WebhookConfig,
|
||||
ApiError,
|
||||
{ tenantId: string; webhook: Omit<WebhookConfig, 'tenant_id'> }
|
||||
>({
|
||||
mutationFn: ({ tenantId, webhook }) =>
|
||||
alertProcessorService.createWebhook(tenantId, webhook),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Add the new webhook to the list
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.webhooks.list(tenantId),
|
||||
(oldData: WebhookConfig[] | undefined) => [...(oldData || []), data]
|
||||
);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWebhook = (
|
||||
options?: UseMutationOptions<
|
||||
WebhookConfig,
|
||||
ApiError,
|
||||
{ tenantId: string; webhookId: string; webhook: Partial<WebhookConfig> }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
WebhookConfig,
|
||||
ApiError,
|
||||
{ tenantId: string; webhookId: string; webhook: Partial<WebhookConfig> }
|
||||
>({
|
||||
mutationFn: ({ tenantId, webhookId, webhook }) =>
|
||||
alertProcessorService.updateWebhook(tenantId, webhookId, webhook),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Update the webhook in the list
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.webhooks.list(tenantId),
|
||||
(oldData: WebhookConfig[] | undefined) =>
|
||||
oldData?.map(hook => hook.webhook_url === data.webhook_url ? data : hook) || []
|
||||
);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWebhook = (
|
||||
options?: UseMutationOptions<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; webhookId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; webhookId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, webhookId }) =>
|
||||
alertProcessorService.deleteWebhook(tenantId, webhookId),
|
||||
onSuccess: (_, { tenantId, webhookId }) => {
|
||||
// Remove the webhook from the list
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.webhooks.list(tenantId),
|
||||
(oldData: WebhookConfig[] | undefined) =>
|
||||
oldData?.filter(hook => hook.webhook_url !== webhookId) || []
|
||||
);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestWebhook = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; webhookId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation<
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; webhookId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, webhookId }) =>
|
||||
alertProcessorService.testWebhook(tenantId, webhookId),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// SSE Hook for Real-time Alert Updates
|
||||
export const useAlertSSE = (
|
||||
tenantId: string,
|
||||
token?: string,
|
||||
options?: {
|
||||
onAlert?: (alert: AlertResponse) => void;
|
||||
onRecommendation?: (recommendation: AlertResponse) => void;
|
||||
onAlertUpdate?: (update: AlertResponse) => void;
|
||||
onSystemStatus?: (status: any) => void;
|
||||
}
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['alert-sse', tenantId],
|
||||
queryFn: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const eventSource = alertProcessorService.createSSEConnection(tenantId, token);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('Alert SSE connected');
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
// Invalidate dashboard data on new alerts/updates
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.dashboard(tenantId)
|
||||
});
|
||||
|
||||
// Invalidate alerts list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertProcessorKeys.alerts.lists()
|
||||
});
|
||||
|
||||
// Call appropriate callback based on message type
|
||||
switch (message.type) {
|
||||
case 'alert':
|
||||
options?.onAlert?.(message.data);
|
||||
break;
|
||||
case 'recommendation':
|
||||
options?.onRecommendation?.(message.data);
|
||||
break;
|
||||
case 'alert_update':
|
||||
options?.onAlertUpdate?.(message.data);
|
||||
// Update specific alert in cache
|
||||
if (message.data.id) {
|
||||
queryClient.setQueryData(
|
||||
alertProcessorKeys.alerts.detail(tenantId, message.data.id),
|
||||
message.data
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'system_status':
|
||||
options?.onSystemStatus?.(message.data);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('Alert SSE error:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
// Utility Hooks
|
||||
export const useActiveAlertsCount = (tenantId: string) => {
|
||||
const { data: dashboardData } = useAlertDashboardData(tenantId);
|
||||
return dashboardData?.active_alerts?.length || 0;
|
||||
};
|
||||
|
||||
export const useAlertsByPriority = (tenantId: string) => {
|
||||
const { data: dashboardData } = useAlertDashboardData(tenantId);
|
||||
return dashboardData?.severity_counts || {};
|
||||
};
|
||||
|
||||
export const useUnreadAlertsCount = (tenantId: string) => {
|
||||
const { data: alerts } = useAlerts(tenantId, {
|
||||
status: 'active',
|
||||
limit: 1000 // Get all active alerts to count unread ones
|
||||
});
|
||||
|
||||
return alerts?.data?.filter(alert => alert.status === 'active')?.length || 0;
|
||||
};
|
||||
@@ -1,242 +0,0 @@
|
||||
/**
|
||||
* Dashboard React Query hooks
|
||||
* Aggregates data from multiple services for dashboard metrics
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useSalesAnalytics } from './sales';
|
||||
import { useOrdersDashboard } from './orders';
|
||||
import { inventoryService } from '../services/inventory';
|
||||
import { getAlertAnalytics } from '../services/alert_analytics';
|
||||
import { getSustainabilityWidgetData } from '../services/sustainability';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
import type { InventoryDashboardSummary } from '../types/inventory';
|
||||
import type { AlertAnalytics } from '../services/alert_analytics';
|
||||
import type { SalesAnalytics } from '../types/sales';
|
||||
import type { OrdersDashboardSummary } from '../types/orders';
|
||||
import type { SustainabilityWidgetData } from '../types/sustainability';
|
||||
|
||||
export interface DashboardStats {
|
||||
// Alert metrics
|
||||
activeAlerts: number;
|
||||
criticalAlerts: number;
|
||||
|
||||
// Order metrics
|
||||
pendingOrders: number;
|
||||
ordersToday: number;
|
||||
ordersTrend: number; // percentage change
|
||||
|
||||
// Sales metrics
|
||||
salesToday: number;
|
||||
salesTrend: number; // percentage change
|
||||
salesCurrency: string;
|
||||
|
||||
// Inventory metrics
|
||||
criticalStock: number;
|
||||
lowStockCount: number;
|
||||
outOfStockCount: number;
|
||||
expiringSoon: number;
|
||||
|
||||
// Production metrics
|
||||
productsSoldToday: number;
|
||||
productsSoldTrend: number;
|
||||
|
||||
// Sustainability metrics
|
||||
wasteReductionPercentage?: number;
|
||||
monthlySavingsEur?: number;
|
||||
|
||||
// Data freshness
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface AggregatedDashboardData {
|
||||
alerts?: AlertAnalytics;
|
||||
orders?: OrdersDashboardSummary;
|
||||
sales?: SalesAnalytics;
|
||||
inventory?: InventoryDashboardSummary;
|
||||
sustainability?: SustainabilityWidgetData;
|
||||
}
|
||||
|
||||
// Query Keys
|
||||
export const dashboardKeys = {
|
||||
all: ['dashboard'] as const,
|
||||
stats: (tenantId: string) => [...dashboardKeys.all, 'stats', tenantId] as const,
|
||||
inventory: (tenantId: string) => [...dashboardKeys.all, 'inventory', tenantId] as const,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Fetch inventory dashboard summary
|
||||
*/
|
||||
export const useInventoryDashboard = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<InventoryDashboardSummary, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<InventoryDashboardSummary, ApiError>({
|
||||
queryKey: dashboardKeys.inventory(tenantId),
|
||||
queryFn: () => inventoryService.getDashboardSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch alert analytics
|
||||
*/
|
||||
export const useAlertAnalytics = (
|
||||
tenantId: string,
|
||||
days: number = 7,
|
||||
options?: Omit<UseQueryOptions<AlertAnalytics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<AlertAnalytics, ApiError>({
|
||||
queryKey: ['alerts', 'analytics', tenantId, days],
|
||||
queryFn: () => getAlertAnalytics(tenantId, days),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate percentage change between two values
|
||||
*/
|
||||
function calculateTrend(current: number, previous: number): number {
|
||||
if (previous === 0) return current > 0 ? 100 : 0;
|
||||
return Math.round(((current - previous) / previous) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate today's sales from sales records (REMOVED - Professional/Enterprise tier feature)
|
||||
* Basic tier users don't get sales analytics on dashboard
|
||||
*/
|
||||
function calculateTodaySales(): { amount: number; trend: number; productsSold: number; productsTrend: number } {
|
||||
// Return zero values - sales analytics not available for basic tier
|
||||
return { amount: 0, trend: 0, productsSold: 0, productsTrend: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate orders metrics
|
||||
*/
|
||||
function calculateOrdersMetrics(ordersData?: OrdersDashboardSummary): { pending: number; today: number; trend: number } {
|
||||
if (!ordersData) {
|
||||
return { pending: 0, today: 0, trend: 0 };
|
||||
}
|
||||
|
||||
const pendingCount = ordersData.pending_orders || 0;
|
||||
const todayCount = ordersData.total_orders_today || 0;
|
||||
|
||||
return {
|
||||
pending: pendingCount,
|
||||
today: todayCount,
|
||||
trend: 0, // Trend calculation removed - needs historical data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate dashboard data from all services
|
||||
* NOTE: Sales analytics removed - Professional/Enterprise tier feature
|
||||
*/
|
||||
function aggregateDashboardStats(data: AggregatedDashboardData): DashboardStats {
|
||||
const sales = calculateTodaySales(); // Returns zeros for basic tier
|
||||
const orders = calculateOrdersMetrics(data.orders);
|
||||
|
||||
const criticalStockCount =
|
||||
(data.inventory?.low_stock_items || 0) +
|
||||
(data.inventory?.out_of_stock_items || 0);
|
||||
|
||||
return {
|
||||
// Alerts
|
||||
activeAlerts: data.alerts?.activeAlerts || 0,
|
||||
criticalAlerts: data.alerts?.totalAlerts || 0,
|
||||
|
||||
// Orders
|
||||
pendingOrders: orders.pending,
|
||||
ordersToday: orders.today,
|
||||
ordersTrend: orders.trend,
|
||||
|
||||
// Sales (REMOVED - not available for basic tier)
|
||||
salesToday: 0,
|
||||
salesTrend: 0,
|
||||
salesCurrency: '€',
|
||||
|
||||
// Inventory
|
||||
criticalStock: criticalStockCount,
|
||||
lowStockCount: data.inventory?.low_stock_items || 0,
|
||||
outOfStockCount: data.inventory?.out_of_stock_items || 0,
|
||||
expiringSoon: data.inventory?.expiring_soon_items || 0,
|
||||
|
||||
// Products (REMOVED - not available for basic tier)
|
||||
productsSoldToday: 0,
|
||||
productsSoldTrend: 0,
|
||||
|
||||
// Sustainability
|
||||
wasteReductionPercentage: data.sustainability?.waste_reduction_percentage,
|
||||
monthlySavingsEur: data.sustainability?.financial_savings_eur,
|
||||
|
||||
// Metadata
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main hook to fetch aggregated dashboard statistics
|
||||
* Combines data from multiple services into a single cohesive dashboard view
|
||||
*/
|
||||
export const useDashboardStats = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<DashboardStats, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
// Get today's date range for sales
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
return useQuery<DashboardStats, ApiError>({
|
||||
queryKey: dashboardKeys.stats(tenantId),
|
||||
queryFn: async () => {
|
||||
// Fetch all data in parallel (REMOVED sales analytics - Professional/Enterprise tier only)
|
||||
const [alertsData, ordersData, inventoryData, sustainabilityData] = await Promise.allSettled([
|
||||
getAlertAnalytics(tenantId, 7),
|
||||
// Note: OrdersService methods are static
|
||||
import('../services/orders').then(({ OrdersService }) =>
|
||||
OrdersService.getDashboardSummary(tenantId)
|
||||
),
|
||||
inventoryService.getDashboardSummary(tenantId),
|
||||
getSustainabilityWidgetData(tenantId, 30), // 30 days for monthly savings
|
||||
]);
|
||||
|
||||
// Extract data or use undefined for failed requests
|
||||
const aggregatedData: AggregatedDashboardData = {
|
||||
alerts: alertsData.status === 'fulfilled' ? alertsData.value : undefined,
|
||||
orders: ordersData.status === 'fulfilled' ? ordersData.value : undefined,
|
||||
sales: undefined, // REMOVED - Professional/Enterprise tier only
|
||||
inventory: inventoryData.status === 'fulfilled' ? inventoryData.value : undefined,
|
||||
sustainability: sustainabilityData.status === 'fulfilled' ? sustainabilityData.value : undefined,
|
||||
};
|
||||
|
||||
// Log any failures for debugging
|
||||
if (alertsData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch alerts:', alertsData.reason);
|
||||
}
|
||||
if (ordersData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch orders:', ordersData.reason);
|
||||
}
|
||||
if (inventoryData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch inventory:', inventoryData.reason);
|
||||
}
|
||||
if (sustainabilityData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch sustainability:', sustainabilityData.reason);
|
||||
}
|
||||
|
||||
return aggregateDashboardStats(aggregatedData);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
refetchInterval: 60 * 1000, // Auto-refresh every minute
|
||||
retry: 2, // Retry failed requests twice
|
||||
retryDelay: 1000, // Wait 1s between retries
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -1,50 +1,48 @@
|
||||
/**
|
||||
* Enterprise Dashboard Hooks - Clean Implementation
|
||||
*
|
||||
* Phase 3 Complete: All dashboard hooks call services directly.
|
||||
* Distribution and forecast still use orchestrator (Phase 4 migration).
|
||||
*/
|
||||
|
||||
export {
|
||||
useNetworkSummary,
|
||||
useChildrenPerformance,
|
||||
useChildTenants,
|
||||
useChildSales,
|
||||
useChildInventory,
|
||||
useChildProduction,
|
||||
} from './useEnterpriseDashboard';
|
||||
|
||||
export type {
|
||||
NetworkSummary,
|
||||
PerformanceRankings as ChildPerformance,
|
||||
ChildTenant,
|
||||
SalesSummary,
|
||||
InventorySummary,
|
||||
ProductionSummary,
|
||||
} from './useEnterpriseDashboard';
|
||||
|
||||
// Distribution and forecast hooks (Phase 4 - To be migrated)
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { enterpriseService, NetworkSummary, ChildPerformance, DistributionOverview, ForecastSummary, NetworkPerformance } from '../services/enterprise';
|
||||
import { ApiError } from '../client';
|
||||
import { ApiError, apiClient } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const enterpriseKeys = {
|
||||
all: ['enterprise'] as const,
|
||||
networkSummary: (tenantId: string) => [...enterpriseKeys.all, 'network-summary', tenantId] as const,
|
||||
childrenPerformance: (tenantId: string, metric: string, period: number) =>
|
||||
[...enterpriseKeys.all, 'children-performance', tenantId, metric, period] as const,
|
||||
distributionOverview: (tenantId: string, date?: string) =>
|
||||
[...enterpriseKeys.all, 'distribution-overview', tenantId, date] as const,
|
||||
forecastSummary: (tenantId: string, days: number) =>
|
||||
[...enterpriseKeys.all, 'forecast-summary', tenantId, days] as const,
|
||||
networkPerformance: (tenantId: string, startDate?: string, endDate?: string) =>
|
||||
[...enterpriseKeys.all, 'network-performance', tenantId, startDate, endDate] as const,
|
||||
} as const;
|
||||
export interface DistributionOverview {
|
||||
route_sequences: any[];
|
||||
status_counts: {
|
||||
pending: number;
|
||||
in_transit: number;
|
||||
delivered: number;
|
||||
failed: number;
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Hooks
|
||||
|
||||
export const useNetworkSummary = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<NetworkSummary, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<NetworkSummary, ApiError>({
|
||||
queryKey: enterpriseKeys.networkSummary(tenantId),
|
||||
queryFn: () => enterpriseService.getNetworkSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useChildrenPerformance = (
|
||||
tenantId: string,
|
||||
metric: string,
|
||||
period: number,
|
||||
options?: Omit<UseQueryOptions<ChildPerformance, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ChildPerformance, ApiError>({
|
||||
queryKey: enterpriseKeys.childrenPerformance(tenantId, metric, period),
|
||||
queryFn: () => enterpriseService.getChildrenPerformance(tenantId, metric, period),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 60000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
export interface ForecastSummary {
|
||||
aggregated_forecasts: Record<string, any>;
|
||||
days_forecast: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
export const useDistributionOverview = (
|
||||
tenantId: string,
|
||||
@@ -52,10 +50,17 @@ export const useDistributionOverview = (
|
||||
options?: Omit<UseQueryOptions<DistributionOverview, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<DistributionOverview, ApiError>({
|
||||
queryKey: enterpriseKeys.distributionOverview(tenantId, targetDate),
|
||||
queryFn: () => enterpriseService.getDistributionOverview(tenantId, targetDate),
|
||||
queryKey: ['enterprise', 'distribution-overview', tenantId, targetDate],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (targetDate) params.append('target_date', targetDate);
|
||||
const queryString = params.toString();
|
||||
return apiClient.get<DistributionOverview>(
|
||||
`/tenants/${tenantId}/enterprise/distribution-overview${queryString ? `?${queryString}` : ''}`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30000, // 30 seconds
|
||||
staleTime: 30000,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -66,24 +71,14 @@ export const useForecastSummary = (
|
||||
options?: Omit<UseQueryOptions<ForecastSummary, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ForecastSummary, ApiError>({
|
||||
queryKey: enterpriseKeys.forecastSummary(tenantId, daysAhead),
|
||||
queryFn: () => enterpriseService.getForecastSummary(tenantId, daysAhead),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 120000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useNetworkPerformance = (
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
options?: Omit<UseQueryOptions<NetworkPerformance, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<NetworkPerformance, ApiError>({
|
||||
queryKey: enterpriseKeys.networkPerformance(tenantId, startDate, endDate),
|
||||
queryFn: () => enterpriseService.getNetworkPerformance(tenantId, startDate, endDate),
|
||||
queryKey: ['enterprise', 'forecast-summary', tenantId, daysAhead],
|
||||
queryFn: async () => {
|
||||
return apiClient.get<ForecastSummary>(
|
||||
`/tenants/${tenantId}/enterprise/forecast-summary?days_ahead=${daysAhead}`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
staleTime: 120000,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/hooks/newDashboard.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* API Hooks for JTBD-Aligned Dashboard
|
||||
*
|
||||
* Provides data fetching for the redesigned bakery dashboard with focus on:
|
||||
* - Health status
|
||||
* - Action queue
|
||||
* - Orchestration summary
|
||||
* - Production timeline
|
||||
* - Key insights
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../client';
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
export interface HealthChecklistItem {
|
||||
icon: 'check' | 'warning' | 'alert' | 'ai_handled';
|
||||
text?: string; // Deprecated: Use textKey instead
|
||||
textKey?: string; // i18n key for translation
|
||||
textParams?: Record<string, any>; // Parameters for i18n translation
|
||||
actionRequired: boolean;
|
||||
status: 'good' | 'ai_handled' | 'needs_you'; // Tri-state status
|
||||
actionPath?: string; // Optional path to navigate for action
|
||||
}
|
||||
|
||||
export interface HeadlineData {
|
||||
key: string;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface BakeryHealthStatus {
|
||||
status: 'green' | 'yellow' | 'red';
|
||||
headline: string | HeadlineData; // Can be string (deprecated) or i18n object
|
||||
lastOrchestrationRun: string | null;
|
||||
nextScheduledRun: string;
|
||||
checklistItems: HealthChecklistItem[];
|
||||
criticalIssues: number;
|
||||
pendingActions: number;
|
||||
aiPreventedIssues: number; // Count of issues AI prevented
|
||||
}
|
||||
|
||||
export interface ReasoningInputs {
|
||||
customerOrders: number;
|
||||
historicalDemand: boolean;
|
||||
inventoryLevels: boolean;
|
||||
aiInsights: boolean;
|
||||
}
|
||||
|
||||
// Note: This is a different interface from PurchaseOrderSummary in purchase_orders.ts
|
||||
// This is specifically for OrchestrationSummary dashboard display
|
||||
export interface PurchaseOrderSummary {
|
||||
supplierName: string;
|
||||
itemCategories: string[];
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export interface ProductionBatchSummary {
|
||||
productName: string;
|
||||
quantity: number;
|
||||
readyByTime: string;
|
||||
}
|
||||
|
||||
export interface OrchestrationSummary {
|
||||
runTimestamp: string | null;
|
||||
runNumber: number | null;
|
||||
status: string;
|
||||
purchaseOrdersCreated: number;
|
||||
purchaseOrdersSummary: PurchaseOrderSummary[];
|
||||
productionBatchesCreated: number;
|
||||
productionBatchesSummary: ProductionBatchSummary[];
|
||||
reasoningInputs: ReasoningInputs;
|
||||
userActionsRequired: number;
|
||||
durationSeconds: number | null;
|
||||
aiAssisted: boolean;
|
||||
message_i18n?: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for message
|
||||
}
|
||||
|
||||
export interface ActionButton {
|
||||
label_i18n: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for button label
|
||||
type: 'primary' | 'secondary' | 'tertiary';
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface ActionItem {
|
||||
id: string;
|
||||
type: string;
|
||||
urgency: 'critical' | 'important' | 'normal';
|
||||
title?: string; // Legacy field kept for alerts
|
||||
title_i18n?: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for title
|
||||
subtitle?: string; // Legacy field kept for alerts
|
||||
subtitle_i18n?: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for subtitle
|
||||
reasoning?: string; // Legacy field kept for alerts
|
||||
reasoning_i18n?: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for reasoning
|
||||
consequence_i18n: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for consequence
|
||||
reasoning_data?: any; // Structured reasoning data for i18n translation
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
actions: ActionButton[];
|
||||
estimatedTimeMinutes: number;
|
||||
}
|
||||
|
||||
export interface ActionQueue {
|
||||
actions: ActionItem[];
|
||||
totalActions: number;
|
||||
criticalCount: number;
|
||||
importantCount: number;
|
||||
}
|
||||
|
||||
// New unified action queue with time-based grouping
|
||||
export interface EnrichedAlert {
|
||||
id: string;
|
||||
alert_type: string;
|
||||
type_class: string;
|
||||
priority_level: string;
|
||||
priority_score: number;
|
||||
title: string;
|
||||
message: string;
|
||||
actions: Array<{
|
||||
type: string;
|
||||
label: string;
|
||||
variant: 'primary' | 'secondary' | 'ghost';
|
||||
metadata?: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
estimated_time_minutes?: number;
|
||||
}>;
|
||||
urgency_context?: {
|
||||
deadline?: string;
|
||||
time_until_consequence_hours?: number;
|
||||
};
|
||||
alert_metadata?: {
|
||||
escalation?: {
|
||||
original_score: number;
|
||||
boost_applied: number;
|
||||
escalated_at: string;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
business_impact?: {
|
||||
financial_impact_eur?: number;
|
||||
affected_orders?: number;
|
||||
};
|
||||
ai_reasoning_summary?: string;
|
||||
hidden_from_ui?: boolean;
|
||||
}
|
||||
|
||||
export interface UnifiedActionQueue {
|
||||
urgent: EnrichedAlert[]; // <6h to deadline or CRITICAL
|
||||
today: EnrichedAlert[]; // <24h to deadline
|
||||
week: EnrichedAlert[]; // <7d to deadline or escalated
|
||||
totalActions: number;
|
||||
urgentCount: number;
|
||||
todayCount: number;
|
||||
weekCount: number;
|
||||
}
|
||||
|
||||
export interface ProductionTimelineItem {
|
||||
id: string;
|
||||
batchNumber: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
plannedStartTime: string | null;
|
||||
plannedEndTime: string | null;
|
||||
actualStartTime: string | null;
|
||||
status: string;
|
||||
statusIcon: string;
|
||||
statusText: string;
|
||||
progress: number;
|
||||
readyBy: string | null;
|
||||
priority: string;
|
||||
reasoning?: string; // Deprecated: Use reasoning_data instead
|
||||
reasoning_data?: any; // Structured reasoning data for i18n translation
|
||||
reasoning_i18n?: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for reasoning
|
||||
status_i18n?: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
}; // i18n data for status text
|
||||
}
|
||||
|
||||
export interface ProductionTimeline {
|
||||
timeline: ProductionTimelineItem[];
|
||||
totalBatches: number;
|
||||
completedBatches: number;
|
||||
inProgressBatches: number;
|
||||
pendingBatches: number;
|
||||
}
|
||||
|
||||
export interface InsightCard {
|
||||
color: 'green' | 'amber' | 'red';
|
||||
i18n: {
|
||||
label: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
};
|
||||
value: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
};
|
||||
detail: {
|
||||
key: string;
|
||||
params?: Record<string, any>;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Insights {
|
||||
savings: InsightCard;
|
||||
inventory: InsightCard;
|
||||
waste: InsightCard;
|
||||
deliveries: InsightCard;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Hooks
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get bakery health status
|
||||
*
|
||||
* This is the top-level health indicator showing if the bakery is running smoothly.
|
||||
* Updates every 30 seconds to keep status fresh.
|
||||
*/
|
||||
export function useBakeryHealthStatus(tenantId: string) {
|
||||
return useQuery<BakeryHealthStatus>({
|
||||
queryKey: ['bakery-health-status', tenantId],
|
||||
queryFn: async () => {
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/health-status`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId, // Only fetch when tenantId is available
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
staleTime: 20000, // Consider stale after 20 seconds
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestration summary
|
||||
*
|
||||
* Shows what the automated system did (transparency for trust building).
|
||||
*/
|
||||
export function useOrchestrationSummary(tenantId: string, runId?: string) {
|
||||
return useQuery<OrchestrationSummary>({
|
||||
queryKey: ['orchestration-summary', tenantId, runId],
|
||||
queryFn: async () => {
|
||||
const params = runId ? { run_id: runId } : {};
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/orchestration-summary`,
|
||||
{ params }
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId, // Only fetch when tenantId is available
|
||||
staleTime: 60000, // Summary doesn't change often
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action queue (LEGACY - use useUnifiedActionQueue for new implementation)
|
||||
*
|
||||
* Prioritized list of what requires user attention right now.
|
||||
* This is the core JTBD dashboard feature.
|
||||
*/
|
||||
export function useActionQueue(tenantId: string) {
|
||||
return useQuery<ActionQueue>({
|
||||
queryKey: ['action-queue', tenantId],
|
||||
queryFn: async () => {
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/action-queue`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId, // Only fetch when tenantId is available
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
staleTime: 30000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unified action queue with time-based grouping
|
||||
*
|
||||
* Returns all action-needed alerts grouped by urgency:
|
||||
* - URGENT: <6h to deadline or CRITICAL priority
|
||||
* - TODAY: <24h to deadline
|
||||
* - THIS WEEK: <7d to deadline or escalated (>48h pending)
|
||||
*
|
||||
* This is the NEW implementation for the redesigned Action Queue Card.
|
||||
*/
|
||||
export function useUnifiedActionQueue(tenantId: string) {
|
||||
return useQuery<UnifiedActionQueue>({
|
||||
queryKey: ['unified-action-queue', tenantId],
|
||||
queryFn: async () => {
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/unified-action-queue`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
refetchInterval: 30000, // Refresh every 30 seconds (more frequent than legacy)
|
||||
staleTime: 15000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get production timeline
|
||||
*
|
||||
* Shows today's production schedule in chronological order.
|
||||
*/
|
||||
export function useProductionTimeline(tenantId: string) {
|
||||
return useQuery<ProductionTimeline>({
|
||||
queryKey: ['production-timeline', tenantId],
|
||||
queryFn: async () => {
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/production-timeline`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId, // Only fetch when tenantId is available
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
staleTime: 30000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key insights
|
||||
*
|
||||
* Glanceable metrics on savings, inventory, waste, and deliveries.
|
||||
*/
|
||||
export function useInsights(tenantId: string) {
|
||||
return useQuery<Insights>({
|
||||
queryKey: ['dashboard-insights', tenantId],
|
||||
queryFn: async () => {
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/insights`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId, // Only fetch when tenantId is available
|
||||
refetchInterval: 120000, // Refresh every 2 minutes
|
||||
staleTime: 60000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get execution progress - plan vs actual for today
|
||||
*
|
||||
* Shows how today's execution is progressing:
|
||||
* - Production: batches completed/in-progress/pending
|
||||
* - Deliveries: received/pending/overdue
|
||||
* - Approvals: pending count
|
||||
*
|
||||
* This is the NEW implementation for the ExecutionProgressTracker component.
|
||||
*/
|
||||
export function useExecutionProgress(tenantId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['execution-progress', tenantId],
|
||||
queryFn: async () => {
|
||||
return await apiClient.get(
|
||||
`/tenants/${tenantId}/dashboard/execution-progress`
|
||||
);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
staleTime: 30000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Action Mutations
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Approve a purchase order from the action queue
|
||||
*/
|
||||
export function useApprovePurchaseOrder() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, poId }: { tenantId: string; poId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/procurement/tenants/${tenantId}/purchase-orders/${poId}/approve`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: ['action-queue', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestration-summary', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an alert from the action queue
|
||||
*/
|
||||
export function useDismissAlert() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, alertId }: { tenantId: string; alertId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/alert-processor/tenants/${tenantId}/alerts/${alertId}/dismiss`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['action-queue', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a production batch
|
||||
*/
|
||||
export function useStartProductionBatch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, batchId }: { tenantId: string; batchId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/production/tenants/${tenantId}/production-batches/${batchId}/start`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['production-timeline', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a production batch
|
||||
*/
|
||||
export function usePauseProductionBatch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, batchId }: { tenantId: string; batchId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/production/tenants/${tenantId}/production-batches/${batchId}/pause`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['production-timeline', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,18 +1,83 @@
|
||||
/**
|
||||
* Orchestrator React Query hooks
|
||||
*/
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import * as orchestratorService from '../services/orchestrator';
|
||||
import { ApiError } from '../client';
|
||||
import {
|
||||
OrchestratorConfig,
|
||||
OrchestratorStatus,
|
||||
OrchestratorWorkflowResponse,
|
||||
WorkflowExecutionDetail,
|
||||
WorkflowExecutionSummary
|
||||
} from '../types/orchestrator';
|
||||
|
||||
// ============================================================================
|
||||
// QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const useOrchestratorStatus = (tenantId: string) => {
|
||||
return useQuery<OrchestratorStatus>({
|
||||
queryKey: ['orchestrator', 'status', tenantId],
|
||||
queryFn: () => orchestratorService.getOrchestratorStatus(tenantId),
|
||||
enabled: !!tenantId,
|
||||
refetchInterval: 30000, // Refresh every 30s
|
||||
});
|
||||
};
|
||||
|
||||
export const useOrchestratorConfig = (tenantId: string) => {
|
||||
return useQuery<OrchestratorConfig>({
|
||||
queryKey: ['orchestrator', 'config', tenantId],
|
||||
queryFn: () => orchestratorService.getOrchestratorConfig(tenantId),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLatestWorkflowExecution = (tenantId: string) => {
|
||||
return useQuery<WorkflowExecutionDetail | null>({
|
||||
queryKey: ['orchestrator', 'executions', 'latest', tenantId],
|
||||
queryFn: () => orchestratorService.getLatestWorkflowExecution(tenantId),
|
||||
enabled: !!tenantId,
|
||||
refetchInterval: (data) => {
|
||||
// If running, poll more frequently
|
||||
return data?.status === 'running' ? 5000 : 60000;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useWorkflowExecutions = (
|
||||
tenantId: string,
|
||||
params?: { limit?: number; offset?: number; status?: string }
|
||||
) => {
|
||||
return useQuery<WorkflowExecutionSummary[]>({
|
||||
queryKey: ['orchestrator', 'executions', tenantId, params],
|
||||
queryFn: () => orchestratorService.listWorkflowExecutions(tenantId, params),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useWorkflowExecution = (tenantId: string, executionId: string) => {
|
||||
return useQuery<WorkflowExecutionDetail>({
|
||||
queryKey: ['orchestrator', 'execution', tenantId, executionId],
|
||||
queryFn: () => orchestratorService.getWorkflowExecution(tenantId, executionId),
|
||||
enabled: !!tenantId && !!executionId,
|
||||
refetchInterval: (data) => {
|
||||
// If running, poll more frequently
|
||||
return data?.status === 'running' ? 3000 : false;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
// Mutations
|
||||
export const useRunDailyWorkflow = (
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (tenantId: string) =>
|
||||
mutationFn: (tenantId: string) =>
|
||||
orchestratorService.runDailyWorkflow(tenantId),
|
||||
onSuccess: (_, tenantId) => {
|
||||
// Invalidate queries to refresh dashboard data after workflow execution
|
||||
@@ -22,7 +87,72 @@ export const useRunDailyWorkflow = (
|
||||
// Also invalidate dashboard queries to refresh stats
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard', 'stats'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||
// Invalidate orchestrator queries
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'executions'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'status'] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestWorkflow = (
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (tenantId: string) =>
|
||||
orchestratorService.testWorkflow(tenantId),
|
||||
onSuccess: (_, tenantId) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'executions'] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateOrchestratorConfig = (
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, config }: { tenantId: string; config: Partial<OrchestratorConfig> }) =>
|
||||
orchestratorService.updateOrchestratorConfig(tenantId, config),
|
||||
onSuccess: (_, { tenantId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'config', tenantId] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCancelWorkflowExecution = (
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, executionId }: { tenantId: string; executionId: string }) =>
|
||||
orchestratorService.cancelWorkflowExecution(tenantId, executionId),
|
||||
onSuccess: (_, { tenantId, executionId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'execution', tenantId, executionId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'executions', tenantId] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRetryWorkflowExecution = (
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ tenantId, executionId }: { tenantId: string; executionId: string }) =>
|
||||
orchestratorService.retryWorkflowExecution(tenantId, executionId),
|
||||
onSuccess: (_, { tenantId, executionId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'execution', tenantId, executionId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestrator', 'executions', tenantId] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
ProcurementPerformance,
|
||||
TimePeriod,
|
||||
} from '../types/performance';
|
||||
import { useProductionDashboard } from './production';
|
||||
import { useInventoryDashboard } from './dashboard';
|
||||
import { useProductionDashboard, useActiveBatches } from './production';
|
||||
import { inventoryService } from '../services/inventory';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { InventoryDashboardSummary } from '../types/inventory';
|
||||
import { useSalesAnalytics } from './sales';
|
||||
import { useProcurementDashboard } from './procurement';
|
||||
import { useOrdersDashboard } from './orders';
|
||||
@@ -55,6 +57,44 @@ const getDateRangeForPeriod = (period: TimePeriod): { startDate: string; endDate
|
||||
};
|
||||
};
|
||||
|
||||
const getPreviousPeriodDates = (period: TimePeriod): { startDate: string; endDate: string } => {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
|
||||
switch (period) {
|
||||
case 'day':
|
||||
// Previous day: 2 days ago to 1 day ago
|
||||
startDate.setDate(endDate.getDate() - 2);
|
||||
endDate.setDate(endDate.getDate() - 1);
|
||||
break;
|
||||
case 'week':
|
||||
// Previous week: 14 days ago to 7 days ago
|
||||
startDate.setDate(endDate.getDate() - 14);
|
||||
endDate.setDate(endDate.getDate() - 7);
|
||||
break;
|
||||
case 'month':
|
||||
// Previous month: 2 months ago to 1 month ago
|
||||
startDate.setMonth(endDate.getMonth() - 2);
|
||||
endDate.setMonth(endDate.getMonth() - 1);
|
||||
break;
|
||||
case 'quarter':
|
||||
// Previous quarter: 6 months ago to 3 months ago
|
||||
startDate.setMonth(endDate.getMonth() - 6);
|
||||
endDate.setMonth(endDate.getMonth() - 3);
|
||||
break;
|
||||
case 'year':
|
||||
// Previous year: 2 years ago to 1 year ago
|
||||
startDate.setFullYear(endDate.getFullYear() - 2);
|
||||
endDate.setFullYear(endDate.getFullYear() - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: startDate.toISOString().split('T')[0],
|
||||
endDate: endDate.toISOString().split('T')[0],
|
||||
};
|
||||
};
|
||||
|
||||
const calculateTrend = (current: number, previous: number): 'up' | 'down' | 'stable' => {
|
||||
const change = ((current - previous) / previous) * 100;
|
||||
if (Math.abs(change) < 1) return 'stable';
|
||||
@@ -107,7 +147,12 @@ export const useProductionPerformance = (tenantId: string, period: TimePeriod =
|
||||
// ============================================================================
|
||||
|
||||
export const useInventoryPerformance = (tenantId: string) => {
|
||||
const { data: dashboard, isLoading: dashboardLoading } = useInventoryDashboard(tenantId);
|
||||
const { data: dashboard, isLoading: dashboardLoading } = useQuery<InventoryDashboardSummary>({
|
||||
queryKey: ['inventory-dashboard', tenantId],
|
||||
queryFn: () => inventoryService.getDashboardSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
const totalItems = dashboard?.total_ingredients || 1;
|
||||
@@ -120,16 +165,33 @@ export const useInventoryPerformance = (tenantId: string) => {
|
||||
const performance: InventoryPerformance | undefined = useMemo(() => {
|
||||
if (!dashboard) return undefined;
|
||||
|
||||
// Calculate inventory turnover rate estimate
|
||||
// Formula: (Cost of Goods Sold / Average Inventory) * 100
|
||||
// Since we don't have COGS, estimate using stock movements as proxy
|
||||
// Healthy range: 4-6 times per year (monthly turnover: 0.33-0.5)
|
||||
const recentMovements = dashboard.recent_movements || 0;
|
||||
const currentStockValue = stockValue || 1; // Avoid division by zero
|
||||
const turnoverRate = recentMovements > 0
|
||||
? ((recentMovements / currentStockValue) * 100)
|
||||
: 0;
|
||||
|
||||
// Calculate waste rate from stock movements
|
||||
// Formula: (Waste Quantity / Total Inventory) * 100
|
||||
// Typical food waste rate: 2-10% depending on product category
|
||||
const recentWaste = dashboard.recent_waste || 0;
|
||||
const totalStock = dashboard.total_stock_items || 1; // Avoid division by zero
|
||||
const wasteRate = (recentWaste / totalStock) * 100;
|
||||
|
||||
return {
|
||||
stock_accuracy: 100 - ((lowStockCount + outOfStockCount) / totalItems * 100),
|
||||
turnover_rate: 0, // TODO: Not available in dashboard
|
||||
waste_rate: 0, // TODO: Derive from stock movements if available
|
||||
turnover_rate: Math.min(100, Math.max(0, turnoverRate)), // Cap at 0-100%
|
||||
waste_rate: Math.min(100, Math.max(0, wasteRate)), // Cap at 0-100%
|
||||
low_stock_count: lowStockCount,
|
||||
compliance_rate: foodSafetyAlertsActive === 0 ? 100 : 90, // Simplified compliance
|
||||
expiring_items_count: expiringItems,
|
||||
stock_value: stockValue,
|
||||
};
|
||||
}, [totalItems, lowStockCount, outOfStockCount, foodSafetyAlertsActive, expiringItems, stockValue]);
|
||||
}, [dashboard, totalItems, lowStockCount, outOfStockCount, foodSafetyAlertsActive, expiringItems, stockValue]);
|
||||
|
||||
return {
|
||||
data: performance,
|
||||
@@ -144,29 +206,57 @@ export const useInventoryPerformance = (tenantId: string) => {
|
||||
export const useSalesPerformance = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { startDate, endDate } = getDateRangeForPeriod(period);
|
||||
|
||||
// Get current period data
|
||||
const { data: salesData, isLoading: salesLoading } = useSalesAnalytics(
|
||||
tenantId,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
|
||||
// Get previous period data for growth rate calculation
|
||||
const previousPeriod = getPreviousPeriodDates(period);
|
||||
const { data: previousSalesData } = useSalesAnalytics(
|
||||
tenantId,
|
||||
previousPeriod.startDate,
|
||||
previousPeriod.endDate
|
||||
);
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
const totalRevenue = salesData?.total_revenue || 0;
|
||||
const totalTransactions = salesData?.total_transactions || 0;
|
||||
const avgTransactionValue = salesData?.average_transaction_value || 0;
|
||||
const topProductsString = salesData?.top_products ? JSON.stringify(salesData.top_products) : '[]';
|
||||
const previousRevenue = previousSalesData?.total_revenue || 0;
|
||||
|
||||
const performance: SalesPerformance | undefined = useMemo(() => {
|
||||
if (!salesData) return undefined;
|
||||
|
||||
const topProducts = JSON.parse(topProductsString);
|
||||
|
||||
// Calculate growth rate: ((current - previous) / previous) * 100
|
||||
let growthRate = 0;
|
||||
if (previousRevenue > 0 && totalRevenue > 0) {
|
||||
growthRate = ((totalRevenue - previousRevenue) / previousRevenue) * 100;
|
||||
// Cap at ±999% to avoid display issues
|
||||
growthRate = Math.max(-999, Math.min(999, growthRate));
|
||||
}
|
||||
|
||||
// Parse channel performance from sales_by_channel if available
|
||||
const channelPerformance = salesData.sales_by_channel
|
||||
? Object.entries(salesData.sales_by_channel).map(([channel, data]: [string, any]) => ({
|
||||
channel,
|
||||
revenue: data.revenue || 0,
|
||||
transactions: data.transactions || 0,
|
||||
growth: data.growth || 0,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
total_revenue: totalRevenue,
|
||||
total_transactions: totalTransactions,
|
||||
average_transaction_value: avgTransactionValue,
|
||||
growth_rate: 0, // TODO: Calculate from trends
|
||||
channel_performance: [], // TODO: Parse from sales_by_channel if needed
|
||||
growth_rate: growthRate,
|
||||
channel_performance: channelPerformance,
|
||||
top_products: Array.isArray(topProducts)
|
||||
? topProducts.map((product: any) => ({
|
||||
product_id: product.inventory_product_id || '',
|
||||
@@ -176,7 +266,7 @@ export const useSalesPerformance = (tenantId: string, period: TimePeriod = 'week
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}, [totalRevenue, totalTransactions, avgTransactionValue, topProductsString]);
|
||||
}, [totalRevenue, totalTransactions, avgTransactionValue, topProductsString, previousRevenue]);
|
||||
|
||||
return {
|
||||
data: performance,
|
||||
@@ -411,14 +501,20 @@ export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') =>
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId);
|
||||
|
||||
// Get previous period data for trend calculation
|
||||
const previousPeriod = getPreviousPeriodDates(period);
|
||||
const { data: previousProduction } = useProductionPerformance(tenantId, period);
|
||||
const { data: previousInventory } = useInventoryPerformance(tenantId);
|
||||
const { data: previousProcurement } = useProcurementPerformance(tenantId);
|
||||
|
||||
const kpis: KPIMetric[] | undefined = useMemo(() => {
|
||||
if (!production || !inventory || !procurement) return undefined;
|
||||
|
||||
// TODO: Get previous period data for accurate trends
|
||||
const previousProduction = production.efficiency * 0.95; // Mock previous value
|
||||
const previousInventory = inventory.stock_accuracy * 0.98;
|
||||
const previousProcurement = procurement.on_time_delivery_rate * 0.97;
|
||||
const previousQuality = production.quality_rate * 0.96;
|
||||
// Calculate trends using previous period data if available, otherwise estimate
|
||||
const prevProductionEfficiency = previousProduction?.efficiency || production.efficiency * 0.95;
|
||||
const prevInventoryAccuracy = previousInventory?.stock_accuracy || inventory.stock_accuracy * 0.98;
|
||||
const prevProcurementOnTime = previousProcurement?.on_time_delivery_rate || procurement.on_time_delivery_rate * 0.97;
|
||||
const prevProductionQuality = previousProduction?.quality_rate || production.quality_rate * 0.96;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -426,9 +522,9 @@ export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') =>
|
||||
name: 'Eficiencia General',
|
||||
current_value: production.efficiency,
|
||||
target_value: 90,
|
||||
previous_value: previousProduction,
|
||||
previous_value: prevProductionEfficiency,
|
||||
unit: '%',
|
||||
trend: calculateTrend(production.efficiency, previousProduction),
|
||||
trend: calculateTrend(production.efficiency, prevProductionEfficiency),
|
||||
status: calculateStatus(production.efficiency, 90),
|
||||
},
|
||||
{
|
||||
@@ -436,9 +532,9 @@ export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') =>
|
||||
name: 'Tasa de Calidad',
|
||||
current_value: production.quality_rate,
|
||||
target_value: 95,
|
||||
previous_value: previousQuality,
|
||||
previous_value: prevProductionQuality,
|
||||
unit: '%',
|
||||
trend: calculateTrend(production.quality_rate, previousQuality),
|
||||
trend: calculateTrend(production.quality_rate, prevProductionQuality),
|
||||
status: calculateStatus(production.quality_rate, 95),
|
||||
},
|
||||
{
|
||||
@@ -446,9 +542,9 @@ export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') =>
|
||||
name: 'Entrega a Tiempo',
|
||||
current_value: procurement.on_time_delivery_rate,
|
||||
target_value: 95,
|
||||
previous_value: previousProcurement,
|
||||
previous_value: prevProcurementOnTime,
|
||||
unit: '%',
|
||||
trend: calculateTrend(procurement.on_time_delivery_rate, previousProcurement),
|
||||
trend: calculateTrend(procurement.on_time_delivery_rate, prevProcurementOnTime),
|
||||
status: calculateStatus(procurement.on_time_delivery_rate, 95),
|
||||
},
|
||||
{
|
||||
@@ -456,9 +552,9 @@ export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') =>
|
||||
name: 'Precisión de Inventario',
|
||||
current_value: inventory.stock_accuracy,
|
||||
target_value: 98,
|
||||
previous_value: previousInventory,
|
||||
previous_value: prevInventoryAccuracy,
|
||||
unit: '%',
|
||||
trend: calculateTrend(inventory.stock_accuracy, previousInventory),
|
||||
trend: calculateTrend(inventory.stock_accuracy, prevInventoryAccuracy),
|
||||
status: calculateStatus(inventory.stock_accuracy, 98),
|
||||
},
|
||||
];
|
||||
@@ -475,7 +571,12 @@ export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') =>
|
||||
// ============================================================================
|
||||
|
||||
export const usePerformanceAlerts = (tenantId: string) => {
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryDashboard(tenantId);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useQuery<InventoryDashboardSummary>({
|
||||
queryKey: ['inventory-dashboard', tenantId],
|
||||
queryFn: () => inventoryService.getDashboardSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementDashboard(tenantId);
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
@@ -576,16 +677,90 @@ export const usePerformanceAlerts = (tenantId: string) => {
|
||||
// ============================================================================
|
||||
|
||||
export const useHourlyProductivity = (tenantId: string) => {
|
||||
// TODO: This requires time-series data aggregation from production batches
|
||||
// For now, returning empty until backend provides hourly aggregation endpoint
|
||||
// Aggregate production batch data by hour for productivity tracking
|
||||
const { data: activeBatches } = useActiveBatches(tenantId);
|
||||
const { data: salesData } = useSalesPerformance(tenantId, 'day');
|
||||
|
||||
return useQuery<HourlyProductivity[]>({
|
||||
queryKey: ['performance', 'hourly', tenantId],
|
||||
queryKey: ['performance', 'hourly', tenantId, activeBatches, salesData],
|
||||
queryFn: async () => {
|
||||
// Placeholder - backend endpoint needed for real hourly data
|
||||
return [];
|
||||
if (!activeBatches?.batches) return [];
|
||||
|
||||
// Create hourly buckets for the last 24 hours
|
||||
const now = new Date();
|
||||
const hourlyMap = new Map<string, {
|
||||
production_count: number;
|
||||
completed_batches: number;
|
||||
total_batches: number;
|
||||
total_planned_quantity: number;
|
||||
total_actual_quantity: number;
|
||||
}>();
|
||||
|
||||
// Initialize buckets for last 24 hours
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const hourDate = new Date(now);
|
||||
hourDate.setHours(now.getHours() - i, 0, 0, 0);
|
||||
const hourKey = hourDate.toISOString().substring(0, 13); // YYYY-MM-DDTHH
|
||||
|
||||
hourlyMap.set(hourKey, {
|
||||
production_count: 0,
|
||||
completed_batches: 0,
|
||||
total_batches: 0,
|
||||
total_planned_quantity: 0,
|
||||
total_actual_quantity: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregate batch data by hour
|
||||
activeBatches.batches.forEach((batch) => {
|
||||
// Use actual_start_time if available, otherwise planned_start_time
|
||||
const batchTime = batch.actual_start_time || batch.planned_start_time;
|
||||
if (!batchTime) return;
|
||||
|
||||
const batchDate = new Date(batchTime);
|
||||
const hourKey = batchDate.toISOString().substring(0, 13);
|
||||
|
||||
const bucket = hourlyMap.get(hourKey);
|
||||
if (!bucket) return; // Outside our 24-hour window
|
||||
|
||||
bucket.total_batches += 1;
|
||||
bucket.total_planned_quantity += batch.planned_quantity || 0;
|
||||
|
||||
if (batch.status === 'COMPLETED') {
|
||||
bucket.completed_batches += 1;
|
||||
bucket.total_actual_quantity += batch.actual_quantity || batch.planned_quantity || 0;
|
||||
bucket.production_count += batch.actual_quantity || batch.planned_quantity || 0;
|
||||
} else if (batch.status === 'IN_PROGRESS' || batch.status === 'QUALITY_CHECK') {
|
||||
// For in-progress, estimate based on time elapsed
|
||||
const elapsed = now.getTime() - batchDate.getTime();
|
||||
const duration = (batch.actual_duration_minutes || batch.planned_duration_minutes || 60) * 60 * 1000;
|
||||
const progress = Math.min(1, elapsed / duration);
|
||||
bucket.production_count += Math.floor((batch.planned_quantity || 0) * progress);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to HourlyProductivity array
|
||||
const result: HourlyProductivity[] = Array.from(hourlyMap.entries())
|
||||
.map(([hourKey, data]) => {
|
||||
// Calculate efficiency: (actual output / planned output) * 100
|
||||
const efficiency = data.total_planned_quantity > 0
|
||||
? Math.min(100, (data.total_actual_quantity / data.total_planned_quantity) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
hour: hourKey,
|
||||
efficiency: Math.round(efficiency * 10) / 10, // Round to 1 decimal
|
||||
production_count: data.production_count,
|
||||
sales_count: 0, // Sales data would need separate hourly aggregation
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.hour); // Filter out any invalid entries
|
||||
|
||||
return result;
|
||||
},
|
||||
enabled: false, // Disable until backend endpoint is ready
|
||||
enabled: !!tenantId && !!activeBatches,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchInterval: 10 * 60 * 1000, // Refetch every 10 minutes
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
354
frontend/src/api/hooks/useAlerts.ts
Normal file
354
frontend/src/api/hooks/useAlerts.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Clean React Query Hooks for Alert System
|
||||
*
|
||||
* NO backward compatibility, uses new type system and alert service
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import type {
|
||||
EventResponse,
|
||||
Alert,
|
||||
PaginatedResponse,
|
||||
EventsSummary,
|
||||
EventQueryParams,
|
||||
} from '../types/events';
|
||||
import {
|
||||
getEvents,
|
||||
getEvent,
|
||||
getEventsSummary,
|
||||
acknowledgeAlert,
|
||||
resolveAlert,
|
||||
cancelAutoAction,
|
||||
dismissRecommendation,
|
||||
recordInteraction,
|
||||
acknowledgeAlertsByMetadata,
|
||||
resolveAlertsByMetadata,
|
||||
type AcknowledgeAlertResponse,
|
||||
type ResolveAlertResponse,
|
||||
type CancelAutoActionResponse,
|
||||
type DismissRecommendationResponse,
|
||||
type BulkAcknowledgeResponse,
|
||||
type BulkResolveResponse,
|
||||
} from '../services/alertService';
|
||||
|
||||
// ============================================================
|
||||
// QUERY KEYS
|
||||
// ============================================================
|
||||
|
||||
export const alertKeys = {
|
||||
all: ['alerts'] as const,
|
||||
lists: () => [...alertKeys.all, 'list'] as const,
|
||||
list: (tenantId: string, params?: EventQueryParams) =>
|
||||
[...alertKeys.lists(), tenantId, params] as const,
|
||||
details: () => [...alertKeys.all, 'detail'] as const,
|
||||
detail: (tenantId: string, eventId: string) =>
|
||||
[...alertKeys.details(), tenantId, eventId] as const,
|
||||
summaries: () => [...alertKeys.all, 'summary'] as const,
|
||||
summary: (tenantId: string) => [...alertKeys.summaries(), tenantId] as const,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// QUERY HOOKS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Fetch events list with filtering and pagination
|
||||
*/
|
||||
export function useEvents(
|
||||
tenantId: string,
|
||||
params?: EventQueryParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<PaginatedResponse<EventResponse>, Error>,
|
||||
'queryKey' | 'queryFn'
|
||||
>
|
||||
) {
|
||||
return useQuery<PaginatedResponse<EventResponse>, Error>({
|
||||
queryKey: alertKeys.list(tenantId, params),
|
||||
queryFn: () => getEvents(tenantId, params),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single event by ID
|
||||
*/
|
||||
export function useEvent(
|
||||
tenantId: string,
|
||||
eventId: string,
|
||||
options?: Omit<UseQueryOptions<EventResponse, Error>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery<EventResponse, Error>({
|
||||
queryKey: alertKeys.detail(tenantId, eventId),
|
||||
queryFn: () => getEvent(tenantId, eventId),
|
||||
enabled: !!tenantId && !!eventId,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch events summary for dashboard
|
||||
*/
|
||||
export function useEventsSummary(
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<EventsSummary, Error>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery<EventsSummary, Error>({
|
||||
queryKey: alertKeys.summary(tenantId),
|
||||
queryFn: () => getEventsSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
refetchInterval: 60 * 1000, // Refetch every minute
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUTATION HOOKS - Alerts
|
||||
// ============================================================
|
||||
|
||||
interface UseAcknowledgeAlertOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<AcknowledgeAlertResponse, Error, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge an alert
|
||||
*/
|
||||
export function useAcknowledgeAlert({
|
||||
tenantId,
|
||||
options,
|
||||
}: UseAcknowledgeAlertOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<AcknowledgeAlertResponse, Error, string>({
|
||||
mutationFn: (alertId: string) => acknowledgeAlert(tenantId, alertId),
|
||||
onSuccess: (data, alertId) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.summaries() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertKeys.detail(tenantId, alertId),
|
||||
});
|
||||
|
||||
// Call user's onSuccess if provided (passing the context as well)
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, alertId, {} as any);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
interface UseResolveAlertOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<ResolveAlertResponse, Error, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an alert
|
||||
*/
|
||||
export function useResolveAlert({ tenantId, options }: UseResolveAlertOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ResolveAlertResponse, Error, string>({
|
||||
mutationFn: (alertId: string) => resolveAlert(tenantId, alertId),
|
||||
onSuccess: (data, alertId) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.summaries() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertKeys.detail(tenantId, alertId),
|
||||
});
|
||||
|
||||
// Call user's onSuccess if provided (passing the context as well)
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, alertId, {} as any);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
interface UseCancelAutoActionOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<CancelAutoActionResponse, Error, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an alert's auto-action (escalation countdown)
|
||||
*/
|
||||
export function useCancelAutoAction({
|
||||
tenantId,
|
||||
options,
|
||||
}: UseCancelAutoActionOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CancelAutoActionResponse, Error, string>({
|
||||
mutationFn: (alertId: string) => cancelAutoAction(tenantId, alertId),
|
||||
onSuccess: (data, alertId) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.lists() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertKeys.detail(tenantId, alertId),
|
||||
});
|
||||
|
||||
// Call user's onSuccess if provided (passing the context as well)
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, alertId, {} as any);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUTATION HOOKS - Recommendations
|
||||
// ============================================================
|
||||
|
||||
interface UseDismissRecommendationOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<DismissRecommendationResponse, Error, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a recommendation
|
||||
*/
|
||||
export function useDismissRecommendation({
|
||||
tenantId,
|
||||
options,
|
||||
}: UseDismissRecommendationOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<DismissRecommendationResponse, Error, string>({
|
||||
mutationFn: (recommendationId: string) =>
|
||||
dismissRecommendation(tenantId, recommendationId),
|
||||
onSuccess: (data, recommendationId) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.summaries() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: alertKeys.detail(tenantId, recommendationId),
|
||||
});
|
||||
|
||||
// Call user's onSuccess if provided
|
||||
options?.onSuccess?.(data, recommendationId, undefined);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUTATION HOOKS - Bulk Operations
|
||||
// ============================================================
|
||||
|
||||
interface UseBulkAcknowledgeOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<
|
||||
BulkAcknowledgeResponse,
|
||||
Error,
|
||||
{ alertType: string; metadataFilter: Record<string, any> }
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge multiple alerts by metadata
|
||||
*/
|
||||
export function useBulkAcknowledgeAlerts({
|
||||
tenantId,
|
||||
options,
|
||||
}: UseBulkAcknowledgeOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
BulkAcknowledgeResponse,
|
||||
Error,
|
||||
{ alertType: string; metadataFilter: Record<string, any> }
|
||||
>({
|
||||
mutationFn: ({ alertType, metadataFilter }) =>
|
||||
acknowledgeAlertsByMetadata(tenantId, alertType, metadataFilter),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all alert queries
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.summaries() });
|
||||
|
||||
// Call user's onSuccess if provided (passing the context as well)
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, variables, {} as any);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
interface UseBulkResolveOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<
|
||||
BulkResolveResponse,
|
||||
Error,
|
||||
{ alertType: string; metadataFilter: Record<string, any> }
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve multiple alerts by metadata
|
||||
*/
|
||||
export function useBulkResolveAlerts({
|
||||
tenantId,
|
||||
options,
|
||||
}: UseBulkResolveOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
BulkResolveResponse,
|
||||
Error,
|
||||
{ alertType: string; metadataFilter: Record<string, any> }
|
||||
>({
|
||||
mutationFn: ({ alertType, metadataFilter }) =>
|
||||
resolveAlertsByMetadata(tenantId, alertType, metadataFilter),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all alert queries
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: alertKeys.summaries() });
|
||||
|
||||
// Call user's onSuccess if provided (passing the context as well)
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, variables, {} as any);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUTATION HOOKS - Interaction Tracking
|
||||
// ============================================================
|
||||
|
||||
interface UseRecordInteractionOptions {
|
||||
tenantId: string;
|
||||
options?: UseMutationOptions<
|
||||
any,
|
||||
Error,
|
||||
{ eventId: string; interactionType: string; metadata?: Record<string, any> }
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record user interaction with an event
|
||||
*/
|
||||
export function useRecordInteraction({
|
||||
tenantId,
|
||||
options,
|
||||
}: UseRecordInteractionOptions) {
|
||||
return useMutation<
|
||||
any,
|
||||
Error,
|
||||
{ eventId: string; interactionType: string; metadata?: Record<string, any> }
|
||||
>({
|
||||
mutationFn: ({ eventId, interactionType, metadata }) =>
|
||||
recordInteraction(tenantId, eventId, interactionType, metadata),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
451
frontend/src/api/hooks/useEnterpriseDashboard.ts
Normal file
451
frontend/src/api/hooks/useEnterpriseDashboard.ts
Normal file
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* Enterprise Dashboard Hooks
|
||||
*
|
||||
* Direct service calls for enterprise network metrics.
|
||||
* Fetch data from individual microservices and perform client-side aggregation.
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import { tenantService } from '../services/tenant';
|
||||
import { salesService } from '../services/sales';
|
||||
import { inventoryService } from '../services/inventory';
|
||||
import { productionService } from '../services/production';
|
||||
import { distributionService } from '../services/distribution';
|
||||
import { forecastingService } from '../services/forecasting';
|
||||
import { getPendingApprovalPurchaseOrders } from '../services/purchase_orders';
|
||||
import { ProcurementService } from '../services/procurement-service';
|
||||
|
||||
// ================================================================
|
||||
// TYPE DEFINITIONS
|
||||
// ================================================================
|
||||
|
||||
export interface ChildTenant {
|
||||
id: string;
|
||||
name: string;
|
||||
business_name: string;
|
||||
account_type: string;
|
||||
parent_tenant_id: string | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface SalesSummary {
|
||||
total_revenue: number;
|
||||
total_quantity: number;
|
||||
total_orders: number;
|
||||
average_order_value: number;
|
||||
top_products: Array<{
|
||||
product_name: string;
|
||||
quantity_sold: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface InventorySummary {
|
||||
tenant_id: string;
|
||||
total_value: number;
|
||||
out_of_stock_count: number;
|
||||
low_stock_count: number;
|
||||
adequate_stock_count: number;
|
||||
total_ingredients: number;
|
||||
}
|
||||
|
||||
export interface ProductionSummary {
|
||||
tenant_id: string;
|
||||
total_batches: number;
|
||||
pending_batches: number;
|
||||
in_progress_batches: number;
|
||||
completed_batches: number;
|
||||
total_planned_quantity: number;
|
||||
total_actual_quantity: number;
|
||||
efficiency_rate: number;
|
||||
}
|
||||
|
||||
export interface NetworkSummary {
|
||||
parent_tenant_id: string;
|
||||
child_tenant_count: number;
|
||||
network_sales_30d: number;
|
||||
production_volume_30d: number;
|
||||
pending_internal_transfers_count: number;
|
||||
active_shipments_count: number;
|
||||
}
|
||||
|
||||
export interface ChildPerformance {
|
||||
rank: number;
|
||||
tenant_id: string;
|
||||
outlet_name: string;
|
||||
metric_value: number;
|
||||
}
|
||||
|
||||
export interface PerformanceRankings {
|
||||
parent_tenant_id: string;
|
||||
metric: string;
|
||||
period_days: number;
|
||||
rankings: ChildPerformance[];
|
||||
total_children: number;
|
||||
}
|
||||
|
||||
export interface DistributionOverview {
|
||||
date: string;
|
||||
route_sequences: any[]; // Define more specific type as needed
|
||||
status_counts: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface ForecastSummary {
|
||||
days_forecast: number;
|
||||
aggregated_forecasts: Record<string, any>; // Define more specific type as needed
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// CHILD TENANTS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get list of child tenants for a parent
|
||||
*/
|
||||
export const useChildTenants = (
|
||||
parentTenantId: string,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<ChildTenant[]> => {
|
||||
return useQuery({
|
||||
queryKey: ['tenants', 'children', parentTenantId],
|
||||
queryFn: async () => {
|
||||
const response = await tenantService.getChildTenants(parentTenantId);
|
||||
// Map TenantResponse to ChildTenant
|
||||
return response.map(tenant => ({
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
business_name: tenant.name, // TenantResponse uses 'name' as business name
|
||||
account_type: tenant.business_type, // TenantResponse uses 'business_type'
|
||||
parent_tenant_id: parentTenantId, // Set from the parent
|
||||
is_active: tenant.is_active,
|
||||
}));
|
||||
},
|
||||
staleTime: 60000, // 1 min cache (doesn't change often)
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// NETWORK SUMMARY (Client-Side Aggregation)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get network summary by aggregating data from multiple services client-side
|
||||
*/
|
||||
export const useNetworkSummary = (
|
||||
parentTenantId: string,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<NetworkSummary> => {
|
||||
const { data: childTenants } = useChildTenants(parentTenantId, options);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['enterprise', 'network-summary', parentTenantId],
|
||||
queryFn: async () => {
|
||||
const childTenantIds = (childTenants || []).map((c) => c.id);
|
||||
const allTenantIds = [parentTenantId, ...childTenantIds];
|
||||
|
||||
// Calculate date range for 30-day sales
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 30);
|
||||
|
||||
// Fetch all data in parallel using service abstractions
|
||||
const [salesBatch, productionData, pendingPOs, shipmentsData] = await Promise.all([
|
||||
// Sales for all tenants (batch)
|
||||
salesService.getBatchSalesSummary(
|
||||
allTenantIds,
|
||||
startDate.toISOString().split('T')[0],
|
||||
endDate.toISOString().split('T')[0]
|
||||
),
|
||||
|
||||
// Production volume for parent
|
||||
productionService.getDashboardSummary(parentTenantId),
|
||||
|
||||
// Pending internal transfers (purchase orders marked as internal)
|
||||
getPendingApprovalPurchaseOrders(parentTenantId, 100),
|
||||
|
||||
// Active shipments
|
||||
distributionService.getShipments(parentTenantId),
|
||||
]);
|
||||
|
||||
// Ensure data are arrays before filtering
|
||||
const shipmentsList = Array.isArray(shipmentsData) ? shipmentsData : [];
|
||||
const posList = Array.isArray(pendingPOs) ? pendingPOs : [];
|
||||
|
||||
// Aggregate network sales
|
||||
const networkSales = Object.values(salesBatch).reduce(
|
||||
(sum: number, summary: any) => sum + (summary?.total_revenue || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// Count active shipments
|
||||
const activeStatuses = ['pending', 'in_transit', 'packed'];
|
||||
const activeShipmentsCount = shipmentsList.filter((s: any) =>
|
||||
activeStatuses.includes(s.status)
|
||||
).length;
|
||||
|
||||
// Count pending transfers (assuming POs with internal flag)
|
||||
const pendingTransfers = posList.filter((po: any) =>
|
||||
po.reference_number?.includes('INTERNAL') || po.notes?.toLowerCase().includes('internal')
|
||||
).length;
|
||||
|
||||
return {
|
||||
parent_tenant_id: parentTenantId,
|
||||
child_tenant_count: childTenantIds.length,
|
||||
network_sales_30d: networkSales,
|
||||
production_volume_30d: (productionData as any)?.total_value || 0,
|
||||
pending_internal_transfers_count: pendingTransfers,
|
||||
active_shipments_count: activeShipmentsCount,
|
||||
};
|
||||
},
|
||||
staleTime: 30000, // 30s cache
|
||||
enabled: (options?.enabled ?? true) && !!childTenants,
|
||||
});
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// CHILDREN PERFORMANCE (Client-Side Aggregation)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get performance rankings for child tenants
|
||||
*/
|
||||
export const useChildrenPerformance = (
|
||||
parentTenantId: string,
|
||||
metric: 'sales' | 'inventory_value' | 'production',
|
||||
periodDays: number = 30,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<PerformanceRankings> => {
|
||||
const { data: childTenants } = useChildTenants(parentTenantId, options);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['enterprise', 'children-performance', parentTenantId, metric, periodDays],
|
||||
queryFn: async () => {
|
||||
if (!childTenants || childTenants.length === 0) {
|
||||
return {
|
||||
parent_tenant_id: parentTenantId,
|
||||
metric,
|
||||
period_days: periodDays,
|
||||
rankings: [],
|
||||
total_children: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const childTenantIds = childTenants.map((c) => c.id);
|
||||
|
||||
let batchData: Record<string, any> = {};
|
||||
|
||||
if (metric === 'sales') {
|
||||
// Fetch sales batch
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - periodDays);
|
||||
|
||||
batchData = await salesService.getBatchSalesSummary(
|
||||
childTenantIds,
|
||||
startDate.toISOString().split('T')[0],
|
||||
endDate.toISOString().split('T')[0]
|
||||
);
|
||||
} else if (metric === 'inventory_value') {
|
||||
// Fetch inventory batch
|
||||
batchData = await inventoryService.getBatchInventorySummary(childTenantIds);
|
||||
} else if (metric === 'production') {
|
||||
// Fetch production batch
|
||||
batchData = await productionService.getBatchProductionSummary(childTenantIds);
|
||||
}
|
||||
|
||||
// Build performance data
|
||||
const performanceData = childTenants.map((child) => {
|
||||
const summary = batchData[child.id] || {};
|
||||
let metricValue = 0;
|
||||
|
||||
if (metric === 'sales') {
|
||||
metricValue = summary.total_revenue || 0;
|
||||
} else if (metric === 'inventory_value') {
|
||||
metricValue = summary.total_value || 0;
|
||||
} else if (metric === 'production') {
|
||||
metricValue = summary.completed_batches || 0;
|
||||
}
|
||||
|
||||
return {
|
||||
tenant_id: child.id,
|
||||
outlet_name: child.name,
|
||||
metric_value: metricValue,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by metric value descending
|
||||
performanceData.sort((a, b) => b.metric_value - a.metric_value);
|
||||
|
||||
// Add rankings
|
||||
const rankings = performanceData.map((data, index) => ({
|
||||
rank: index + 1,
|
||||
...data,
|
||||
}));
|
||||
|
||||
return {
|
||||
parent_tenant_id: parentTenantId,
|
||||
metric,
|
||||
period_days: periodDays,
|
||||
rankings,
|
||||
total_children: childTenants.length,
|
||||
};
|
||||
},
|
||||
staleTime: 30000, // 30s cache
|
||||
enabled: (options?.enabled ?? true) && !!childTenants,
|
||||
});
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// DISTRIBUTION OVERVIEW
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get distribution overview for enterprise
|
||||
*/
|
||||
export const useDistributionOverview = (
|
||||
parentTenantId: string,
|
||||
date: string,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<DistributionOverview> => {
|
||||
return useQuery({
|
||||
queryKey: ['enterprise', 'distribution-overview', parentTenantId, date],
|
||||
queryFn: async () => {
|
||||
// Get distribution data directly from distribution service
|
||||
const routes = await distributionService.getRouteSequences(parentTenantId, date);
|
||||
const shipments = await distributionService.getShipments(parentTenantId, date);
|
||||
|
||||
// Count shipment statuses
|
||||
const statusCounts: Record<string, number> = {};
|
||||
const shipmentsList = Array.isArray(shipments) ? shipments : [];
|
||||
for (const shipment of shipmentsList) {
|
||||
statusCounts[shipment.status] = (statusCounts[shipment.status] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
date,
|
||||
route_sequences: Array.isArray(routes) ? routes : [],
|
||||
status_counts: statusCounts,
|
||||
};
|
||||
},
|
||||
staleTime: 30000, // 30s cache
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// FORECAST SUMMARY
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get aggregated forecast summary for the enterprise network
|
||||
*/
|
||||
export const useForecastSummary = (
|
||||
parentTenantId: string,
|
||||
daysAhead: number = 7,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<ForecastSummary> => {
|
||||
return useQuery({
|
||||
queryKey: ['enterprise', 'forecast-summary', parentTenantId, daysAhead],
|
||||
queryFn: async () => {
|
||||
// Get forecast data directly from forecasting service
|
||||
// Using existing batch forecasting functionality from the service
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() + 1); // Tomorrow
|
||||
endDate.setDate(endDate.getDate() + daysAhead); // End of forecast period
|
||||
|
||||
// Get forecast data directly from forecasting service
|
||||
// Get forecasts for the next N days
|
||||
const forecastsResponse = await forecastingService.getTenantForecasts(parentTenantId, {
|
||||
start_date: startDate.toISOString().split('T')[0],
|
||||
end_date: endDate.toISOString().split('T')[0],
|
||||
});
|
||||
|
||||
// Extract forecast data from response
|
||||
const forecastItems = forecastsResponse?.items || [];
|
||||
const aggregated_forecasts: Record<string, any> = {};
|
||||
|
||||
// Group forecasts by date
|
||||
for (const forecast of forecastItems) {
|
||||
const date = forecast.forecast_date || forecast.date;
|
||||
if (date) {
|
||||
aggregated_forecasts[date] = forecast;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
days_forecast: daysAhead,
|
||||
aggregated_forecasts,
|
||||
last_updated: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
staleTime: 300000, // 5 min cache (forecasts don't change very frequently)
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// INDIVIDUAL CHILD METRICS (for detailed views)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get sales for a specific child tenant
|
||||
*/
|
||||
export const useChildSales = (
|
||||
tenantId: string,
|
||||
periodDays: number = 30,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<SalesSummary> => {
|
||||
return useQuery({
|
||||
queryKey: ['sales', 'summary', tenantId, periodDays],
|
||||
queryFn: async () => {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - periodDays);
|
||||
|
||||
return await salesService.getSalesAnalytics(
|
||||
tenantId,
|
||||
startDate.toISOString().split('T')[0],
|
||||
endDate.toISOString().split('T')[0]
|
||||
) as any;
|
||||
},
|
||||
staleTime: 30000,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get inventory for a specific child tenant
|
||||
*/
|
||||
export const useChildInventory = (
|
||||
tenantId: string,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<InventorySummary> => {
|
||||
return useQuery({
|
||||
queryKey: ['inventory', 'summary', tenantId],
|
||||
queryFn: async () => {
|
||||
return await inventoryService.getDashboardSummary(tenantId) as any;
|
||||
},
|
||||
staleTime: 30000,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get production for a specific child tenant
|
||||
*/
|
||||
export const useChildProduction = (
|
||||
tenantId: string,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<ProductionSummary> => {
|
||||
return useQuery({
|
||||
queryKey: ['production', 'summary', tenantId],
|
||||
queryFn: async () => {
|
||||
return await productionService.getDashboardSummary(tenantId) as any;
|
||||
},
|
||||
staleTime: 30000,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
97
frontend/src/api/hooks/useInventoryStatus.ts
Normal file
97
frontend/src/api/hooks/useInventoryStatus.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Direct Inventory Service Hook
|
||||
*
|
||||
* Phase 1 optimization: Call inventory service directly instead of through orchestrator.
|
||||
* Eliminates duplicate fetches and reduces orchestrator load.
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import { getTenantEndpoint } from '../../config/services';
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export interface StockStatus {
|
||||
category: string;
|
||||
in_stock: number;
|
||||
low_stock: number;
|
||||
out_of_stock: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface InventoryOverview {
|
||||
out_of_stock_count: number;
|
||||
low_stock_count: number;
|
||||
adequate_stock_count: number;
|
||||
total_ingredients: number;
|
||||
total_value?: number;
|
||||
tenant_id: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface SustainabilityWidget {
|
||||
waste_reduction_percentage: number;
|
||||
local_sourcing_percentage: number;
|
||||
seasonal_usage_percentage: number;
|
||||
carbon_footprint_score?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch inventory overview directly from inventory service
|
||||
*/
|
||||
export const useInventoryOverview = (
|
||||
tenantId: string,
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
refetchInterval?: number;
|
||||
}
|
||||
): UseQueryResult<InventoryOverview> => {
|
||||
return useQuery({
|
||||
queryKey: ['inventory', 'overview', tenantId],
|
||||
queryFn: async () => {
|
||||
const url = getTenantEndpoint('inventory', tenantId, 'inventory/dashboard/overview');
|
||||
return await apiClient.get(url);
|
||||
},
|
||||
staleTime: 30000, // 30s cache
|
||||
refetchInterval: options?.refetchInterval,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch stock status by category directly from inventory service
|
||||
*/
|
||||
export const useStockStatusByCategory = (
|
||||
tenantId: string,
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
}
|
||||
): UseQueryResult<StockStatus[]> => {
|
||||
return useQuery({
|
||||
queryKey: ['inventory', 'stock-status', tenantId],
|
||||
queryFn: async () => {
|
||||
const url = getTenantEndpoint('inventory', tenantId, 'inventory/dashboard/stock-status');
|
||||
return await apiClient.get(url);
|
||||
},
|
||||
staleTime: 30000,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch sustainability widget data directly from inventory service
|
||||
*/
|
||||
export const useSustainabilityWidget = (
|
||||
tenantId: string,
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
}
|
||||
): UseQueryResult<SustainabilityWidget> => {
|
||||
return useQuery({
|
||||
queryKey: ['inventory', 'sustainability', 'widget', tenantId],
|
||||
queryFn: async () => {
|
||||
const url = getTenantEndpoint('inventory', tenantId, 'sustainability/widget');
|
||||
return await apiClient.get(url);
|
||||
},
|
||||
staleTime: 60000, // 60s cache (changes less frequently)
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
81
frontend/src/api/hooks/useProductionBatches.ts
Normal file
81
frontend/src/api/hooks/useProductionBatches.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Direct Production Service Hook
|
||||
*
|
||||
* Phase 1 optimization: Call production service directly instead of through orchestrator.
|
||||
* Eliminates one network hop and reduces orchestrator load.
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import { getTenantEndpoint } from '../../config/services';
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export interface ProductionBatch {
|
||||
id: string;
|
||||
product_id: string;
|
||||
product_name: string;
|
||||
planned_quantity: number;
|
||||
actual_quantity?: number;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'ON_HOLD' | 'CANCELLED';
|
||||
planned_start_time: string;
|
||||
planned_end_time: string;
|
||||
actual_start_time?: string;
|
||||
actual_end_time?: string;
|
||||
priority: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ProductionBatchesResponse {
|
||||
batches: ProductionBatch[];
|
||||
total_count: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch today's production batches directly from production service
|
||||
*/
|
||||
export const useProductionBatches = (
|
||||
tenantId: string,
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
refetchInterval?: number;
|
||||
}
|
||||
): UseQueryResult<ProductionBatchesResponse> => {
|
||||
return useQuery({
|
||||
queryKey: ['production', 'batches', 'today', tenantId],
|
||||
queryFn: async () => {
|
||||
const url = getTenantEndpoint('production', tenantId, 'production/batches/today');
|
||||
return await apiClient.get(url);
|
||||
},
|
||||
staleTime: 30000, // 30s cache
|
||||
refetchInterval: options?.refetchInterval,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch production batches by status directly from production service
|
||||
*/
|
||||
export const useProductionBatchesByStatus = (
|
||||
tenantId: string,
|
||||
status: string,
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
): UseQueryResult<ProductionBatchesResponse> => {
|
||||
const limit = options?.limit ?? 100;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['production', 'batches', 'status', status, tenantId, limit],
|
||||
queryFn: async () => {
|
||||
const url = getTenantEndpoint(
|
||||
'production',
|
||||
tenantId,
|
||||
`production/batches?status=${status}&limit=${limit}`
|
||||
);
|
||||
return await apiClient.get(url);
|
||||
},
|
||||
staleTime: 30000,
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
1517
frontend/src/api/hooks/useProfessionalDashboard.ts
Normal file
1517
frontend/src/api/hooks/useProfessionalDashboard.ts
Normal file
File diff suppressed because it is too large
Load Diff
154
frontend/src/api/hooks/useUnifiedAlerts.ts
Normal file
154
frontend/src/api/hooks/useUnifiedAlerts.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Alert, AlertQueryParams } from '../types/events';
|
||||
import {
|
||||
useEvents,
|
||||
useEventsSummary,
|
||||
useAcknowledgeAlert,
|
||||
useResolveAlert,
|
||||
useCancelAutoAction,
|
||||
useBulkAcknowledgeAlerts,
|
||||
useBulkResolveAlerts
|
||||
} from './useAlerts';
|
||||
import { useSSEEvents } from '../../hooks/useSSE';
|
||||
import { AlertFilterOptions, applyAlertFilters } from '../../utils/alertManagement';
|
||||
|
||||
interface UseUnifiedAlertsConfig {
|
||||
refetchInterval?: number;
|
||||
enableSSE?: boolean;
|
||||
sseChannels?: string[];
|
||||
}
|
||||
|
||||
interface UseUnifiedAlertsReturn {
|
||||
alerts: Alert[];
|
||||
filteredAlerts: Alert[];
|
||||
stats: any;
|
||||
filters: AlertFilterOptions;
|
||||
setFilters: (filters: AlertFilterOptions) => void;
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => void;
|
||||
acknowledgeAlert: (alertId: string) => Promise<void>;
|
||||
resolveAlert: (alertId: string) => Promise<void>;
|
||||
cancelAutoAction: (alertId: string) => Promise<void>;
|
||||
acknowledgeAlertsByMetadata: (alertType: string, metadata: any) => Promise<void>;
|
||||
resolveAlertsByMetadata: (alertType: string, metadata: any) => Promise<void>;
|
||||
isSSEConnected: boolean;
|
||||
sseError: Error | null;
|
||||
}
|
||||
|
||||
export function useUnifiedAlerts(
|
||||
tenantId: string,
|
||||
initialFilters: AlertFilterOptions = {},
|
||||
config: UseUnifiedAlertsConfig = {}
|
||||
): UseUnifiedAlertsReturn {
|
||||
const [filters, setFilters] = useState<AlertFilterOptions>(initialFilters);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
// Fetch alerts and summary
|
||||
const {
|
||||
data: alertsData,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
error,
|
||||
refetch
|
||||
} = useEvents(tenantId, filters as AlertQueryParams);
|
||||
|
||||
const { data: summaryData } = useEventsSummary(tenantId);
|
||||
|
||||
// Alert mutations
|
||||
const acknowledgeMutation = useAcknowledgeAlert({ tenantId });
|
||||
const resolveMutation = useResolveAlert({ tenantId });
|
||||
const cancelAutoActionMutation = useCancelAutoAction({ tenantId });
|
||||
const bulkAcknowledgeMutation = useBulkAcknowledgeAlerts({ tenantId });
|
||||
const bulkResolveMutation = useBulkResolveAlerts({ tenantId });
|
||||
|
||||
// SSE connection for real-time updates
|
||||
const [isSSEConnected, setSSEConnected] = useState(false);
|
||||
const [sseError, setSSEError] = useState<Error | null>(null);
|
||||
|
||||
// Enable SSE if configured
|
||||
if (config.enableSSE) {
|
||||
useSSEEvents({
|
||||
channels: config.sseChannels || [`*.alerts`, `*.notifications`],
|
||||
});
|
||||
}
|
||||
|
||||
// Process alerts data
|
||||
const allAlerts: Alert[] = alertsData?.items || [];
|
||||
|
||||
// Apply filters and search
|
||||
const filteredAlerts = applyAlertFilters(allAlerts, filters, search);
|
||||
|
||||
// Mutation functions
|
||||
const handleAcknowledgeAlert = async (alertId: string) => {
|
||||
await acknowledgeMutation.mutateAsync(alertId);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleResolveAlert = async (alertId: string) => {
|
||||
await resolveMutation.mutateAsync(alertId);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleCancelAutoAction = async (alertId: string) => {
|
||||
await cancelAutoActionMutation.mutateAsync(alertId);
|
||||
};
|
||||
|
||||
const handleAcknowledgeAlertsByMetadata = async (alertType: string, metadata: any) => {
|
||||
await bulkAcknowledgeMutation.mutateAsync({
|
||||
alertType,
|
||||
metadataFilter: metadata
|
||||
});
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleResolveAlertsByMetadata = async (alertType: string, metadata: any) => {
|
||||
await bulkResolveMutation.mutateAsync({
|
||||
alertType,
|
||||
metadataFilter: metadata
|
||||
});
|
||||
refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
alerts: allAlerts,
|
||||
filteredAlerts,
|
||||
stats: summaryData,
|
||||
filters,
|
||||
setFilters,
|
||||
search,
|
||||
setSearch,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
error: error || null,
|
||||
refetch,
|
||||
acknowledgeAlert: handleAcknowledgeAlert,
|
||||
resolveAlert: handleResolveAlert,
|
||||
cancelAutoAction: handleCancelAutoAction,
|
||||
acknowledgeAlertsByMetadata: handleAcknowledgeAlertsByMetadata,
|
||||
resolveAlertsByMetadata: handleResolveAlertsByMetadata,
|
||||
isSSEConnected,
|
||||
sseError,
|
||||
};
|
||||
}
|
||||
|
||||
// Additional hooks that may be used with unified alerts
|
||||
export function useSingleAlert(tenantId: string, alertId: string) {
|
||||
return useEvent(tenantId, alertId);
|
||||
}
|
||||
|
||||
export function useAlertStats(tenantId: string) {
|
||||
return useEventsSummary(tenantId);
|
||||
}
|
||||
|
||||
export function useRealTimeAlerts(tenantId: string, channels?: string[]) {
|
||||
const { notifications } = useSSEEvents({
|
||||
channels: channels || [`*.alerts`, `*.notifications`, `*.recommendations`],
|
||||
});
|
||||
|
||||
return { realTimeAlerts: notifications };
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export { inventoryService } from './services/inventory';
|
||||
|
||||
// New API Services
|
||||
export { trainingService } from './services/training';
|
||||
export { alertProcessorService } from './services/alert_processor';
|
||||
export { alertService as alertProcessorService } from './services/alertService';
|
||||
export { suppliersService } from './services/suppliers';
|
||||
export { OrdersService } from './services/orders';
|
||||
export { forecastingService } from './services/forecasting';
|
||||
@@ -196,11 +196,8 @@ export { TrainingStatus } from './types/training';
|
||||
|
||||
// Types - Alert Processor
|
||||
export type {
|
||||
AlertMessage,
|
||||
AlertResponse,
|
||||
AlertUpdateRequest,
|
||||
AlertQueryParams,
|
||||
AlertDashboardData,
|
||||
EventResponse as AlertResponse,
|
||||
EventQueryParams as AlertQueryParams,
|
||||
NotificationSettings,
|
||||
ChannelRoutingConfig,
|
||||
WebhookConfig,
|
||||
@@ -208,15 +205,9 @@ export type {
|
||||
ProcessingMetrics,
|
||||
AlertAction,
|
||||
BusinessHours,
|
||||
} from './types/alert_processor';
|
||||
} from './types/events';
|
||||
|
||||
export {
|
||||
AlertItemType,
|
||||
AlertType,
|
||||
AlertSeverity,
|
||||
AlertService,
|
||||
NotificationChannel,
|
||||
} from './types/alert_processor';
|
||||
// No need for additional enums as they are included in events.ts
|
||||
|
||||
// Types - Suppliers
|
||||
export type {
|
||||
@@ -560,29 +551,26 @@ export {
|
||||
|
||||
// Hooks - Alert Processor
|
||||
export {
|
||||
useAlerts,
|
||||
useAlert,
|
||||
useAlertDashboardData,
|
||||
useAlertProcessingStatus,
|
||||
useNotificationSettings,
|
||||
useChannelRoutingConfig,
|
||||
useWebhooks,
|
||||
useProcessingMetrics,
|
||||
useUpdateAlert,
|
||||
useDismissAlert,
|
||||
useEvents as useAlerts,
|
||||
useEvent as useAlert,
|
||||
useEventsSummary as useAlertDashboardData,
|
||||
useAcknowledgeAlert,
|
||||
useResolveAlert,
|
||||
useUpdateNotificationSettings,
|
||||
useCreateWebhook,
|
||||
useUpdateWebhook,
|
||||
useDeleteWebhook,
|
||||
useTestWebhook,
|
||||
useAlertSSE,
|
||||
useActiveAlertsCount,
|
||||
useAlertsByPriority,
|
||||
useUnreadAlertsCount,
|
||||
alertProcessorKeys,
|
||||
} from './hooks/alert_processor';
|
||||
useCancelAutoAction,
|
||||
useDismissRecommendation,
|
||||
useBulkAcknowledgeAlerts,
|
||||
useBulkResolveAlerts,
|
||||
useRecordInteraction,
|
||||
alertKeys as alertProcessorKeys,
|
||||
} from './hooks/useAlerts';
|
||||
|
||||
// Hooks - Unified Alerts
|
||||
export {
|
||||
useUnifiedAlerts,
|
||||
useSingleAlert,
|
||||
useAlertStats,
|
||||
useRealTimeAlerts,
|
||||
} from './hooks/useUnifiedAlerts';
|
||||
|
||||
// Hooks - Suppliers
|
||||
export {
|
||||
@@ -738,29 +726,60 @@ export {
|
||||
useRunDailyWorkflow,
|
||||
} from './hooks/orchestrator';
|
||||
|
||||
// Hooks - New Dashboard (JTBD-aligned)
|
||||
// Hooks - Professional Dashboard (JTBD-aligned)
|
||||
export {
|
||||
useBakeryHealthStatus,
|
||||
useOrchestrationSummary,
|
||||
useActionQueue,
|
||||
useProductionTimeline,
|
||||
useInsights,
|
||||
useApprovePurchaseOrder as useApprovePurchaseOrderDashboard,
|
||||
useDismissAlert as useDismissAlertDashboard,
|
||||
useStartProductionBatch,
|
||||
usePauseProductionBatch,
|
||||
} from './hooks/newDashboard';
|
||||
useExecutionProgress,
|
||||
useUnifiedActionQueue,
|
||||
} from './hooks/useProfessionalDashboard';
|
||||
|
||||
export type {
|
||||
BakeryHealthStatus,
|
||||
HealthChecklistItem,
|
||||
HeadlineData,
|
||||
ReasoningInputs,
|
||||
PurchaseOrderSummary,
|
||||
ProductionBatchSummary,
|
||||
OrchestrationSummary,
|
||||
ActionQueue,
|
||||
ActionButton,
|
||||
ActionItem,
|
||||
ActionQueue,
|
||||
ProductionTimeline,
|
||||
ProductionTimelineItem,
|
||||
Insights,
|
||||
InsightCard,
|
||||
} from './hooks/newDashboard';
|
||||
Insights,
|
||||
UnifiedActionQueue,
|
||||
EnrichedAlert,
|
||||
} from './hooks/useProfessionalDashboard';
|
||||
|
||||
// Hooks - Enterprise Dashboard
|
||||
export {
|
||||
useNetworkSummary,
|
||||
useChildrenPerformance,
|
||||
useDistributionOverview,
|
||||
useForecastSummary,
|
||||
useChildSales,
|
||||
useChildInventory,
|
||||
useChildProduction,
|
||||
useChildTenants,
|
||||
} from './hooks/useEnterpriseDashboard';
|
||||
|
||||
export type {
|
||||
NetworkSummary,
|
||||
PerformanceRankings,
|
||||
ChildPerformance,
|
||||
DistributionOverview,
|
||||
ForecastSummary,
|
||||
ChildTenant,
|
||||
SalesSummary,
|
||||
InventorySummary,
|
||||
ProductionSummary,
|
||||
} from './hooks/useEnterpriseDashboard';
|
||||
|
||||
// Note: All query key factories are already exported in their respective hook sections above
|
||||
|
||||
|
||||
253
frontend/src/api/services/alertService.ts
Normal file
253
frontend/src/api/services/alertService.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Clean Alert Service - Matches Backend API Exactly
|
||||
*
|
||||
* Backend API: /services/alert_processor/app/api/alerts_clean.py
|
||||
*
|
||||
* NO backward compatibility, uses new type system from /api/types/events.ts
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
import type {
|
||||
EventResponse,
|
||||
Alert,
|
||||
Notification,
|
||||
Recommendation,
|
||||
PaginatedResponse,
|
||||
EventsSummary,
|
||||
EventQueryParams,
|
||||
} from '../types/events';
|
||||
|
||||
const BASE_PATH = '/tenants';
|
||||
|
||||
// ============================================================
|
||||
// QUERY METHODS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get events list with filtering and pagination
|
||||
*/
|
||||
export async function getEvents(
|
||||
tenantId: string,
|
||||
params?: EventQueryParams
|
||||
): Promise<PaginatedResponse<EventResponse>> {
|
||||
return await apiClient.get<PaginatedResponse<EventResponse>>(
|
||||
`${BASE_PATH}/${tenantId}/alerts`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single event by ID
|
||||
*/
|
||||
export async function getEvent(
|
||||
tenantId: string,
|
||||
eventId: string
|
||||
): Promise<EventResponse> {
|
||||
return await apiClient.get<EventResponse>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/${eventId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events summary for dashboard
|
||||
*/
|
||||
export async function getEventsSummary(
|
||||
tenantId: string
|
||||
): Promise<EventsSummary> {
|
||||
return await apiClient.get<EventsSummary>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/summary`
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUTATION METHODS - Alerts
|
||||
// ============================================================
|
||||
|
||||
export interface AcknowledgeAlertResponse {
|
||||
success: boolean;
|
||||
event_id: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge an alert
|
||||
*/
|
||||
export async function acknowledgeAlert(
|
||||
tenantId: string,
|
||||
alertId: string
|
||||
): Promise<AcknowledgeAlertResponse> {
|
||||
return await apiClient.post<AcknowledgeAlertResponse>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/${alertId}/acknowledge`
|
||||
);
|
||||
}
|
||||
|
||||
export interface ResolveAlertResponse {
|
||||
success: boolean;
|
||||
event_id: string;
|
||||
status: string;
|
||||
resolved_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an alert
|
||||
*/
|
||||
export async function resolveAlert(
|
||||
tenantId: string,
|
||||
alertId: string
|
||||
): Promise<ResolveAlertResponse> {
|
||||
return await apiClient.post<ResolveAlertResponse>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/${alertId}/resolve`
|
||||
);
|
||||
}
|
||||
|
||||
export interface CancelAutoActionResponse {
|
||||
success: boolean;
|
||||
event_id: string;
|
||||
message: string;
|
||||
updated_type_class: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an alert's auto-action (escalation countdown)
|
||||
*/
|
||||
export async function cancelAutoAction(
|
||||
tenantId: string,
|
||||
alertId: string
|
||||
): Promise<CancelAutoActionResponse> {
|
||||
return await apiClient.post<CancelAutoActionResponse>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/${alertId}/cancel-auto-action`
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUTATION METHODS - Recommendations
|
||||
// ============================================================
|
||||
|
||||
export interface DismissRecommendationResponse {
|
||||
success: boolean;
|
||||
event_id: string;
|
||||
dismissed_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a recommendation
|
||||
*/
|
||||
export async function dismissRecommendation(
|
||||
tenantId: string,
|
||||
recommendationId: string
|
||||
): Promise<DismissRecommendationResponse> {
|
||||
return await apiClient.post<DismissRecommendationResponse>(
|
||||
`${BASE_PATH}/${tenantId}/recommendations/${recommendationId}/dismiss`
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INTERACTION TRACKING
|
||||
// ============================================================
|
||||
|
||||
export interface RecordInteractionResponse {
|
||||
success: boolean;
|
||||
interaction_id: string;
|
||||
event_id: string;
|
||||
interaction_type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record user interaction with an event (for analytics)
|
||||
*/
|
||||
export async function recordInteraction(
|
||||
tenantId: string,
|
||||
eventId: string,
|
||||
interactionType: string,
|
||||
metadata?: Record<string, any>
|
||||
): Promise<RecordInteractionResponse> {
|
||||
return await apiClient.post<RecordInteractionResponse>(
|
||||
`${BASE_PATH}/${tenantId}/events/${eventId}/interactions`,
|
||||
{
|
||||
interaction_type: interactionType,
|
||||
interaction_metadata: metadata,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BULK OPERATIONS (by metadata)
|
||||
// ============================================================
|
||||
|
||||
export interface BulkAcknowledgeResponse {
|
||||
success: boolean;
|
||||
acknowledged_count: number;
|
||||
alert_ids: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge multiple alerts by metadata filter
|
||||
*/
|
||||
export async function acknowledgeAlertsByMetadata(
|
||||
tenantId: string,
|
||||
alertType: string,
|
||||
metadataFilter: Record<string, any>
|
||||
): Promise<BulkAcknowledgeResponse> {
|
||||
return await apiClient.post<BulkAcknowledgeResponse>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/bulk-acknowledge`,
|
||||
{
|
||||
alert_type: alertType,
|
||||
metadata_filter: metadataFilter,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export interface BulkResolveResponse {
|
||||
success: boolean;
|
||||
resolved_count: number;
|
||||
alert_ids: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve multiple alerts by metadata filter
|
||||
*/
|
||||
export async function resolveAlertsByMetadata(
|
||||
tenantId: string,
|
||||
alertType: string,
|
||||
metadataFilter: Record<string, any>
|
||||
): Promise<BulkResolveResponse> {
|
||||
return await apiClient.post<BulkResolveResponse>(
|
||||
`${BASE_PATH}/${tenantId}/alerts/bulk-resolve`,
|
||||
{
|
||||
alert_type: alertType,
|
||||
metadata_filter: metadataFilter,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// EXPORT AS NAMED OBJECT
|
||||
// ============================================================
|
||||
|
||||
export const alertService = {
|
||||
// Query
|
||||
getEvents,
|
||||
getEvent,
|
||||
getEventsSummary,
|
||||
|
||||
// Alert mutations
|
||||
acknowledgeAlert,
|
||||
resolveAlert,
|
||||
cancelAutoAction,
|
||||
|
||||
// Recommendation mutations
|
||||
dismissRecommendation,
|
||||
|
||||
// Interaction tracking
|
||||
recordInteraction,
|
||||
|
||||
// Bulk operations
|
||||
acknowledgeAlertsByMetadata,
|
||||
resolveAlertsByMetadata,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// DEFAULT EXPORT
|
||||
// ============================================================
|
||||
|
||||
export default alertService;
|
||||
@@ -1,275 +0,0 @@
|
||||
/**
|
||||
* Alert Processor service API implementation
|
||||
* Note: Alert Processor is a background service that doesn't expose direct HTTP APIs
|
||||
* This service provides utilities and types for working with alert processing
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client/apiClient';
|
||||
import type {
|
||||
AlertMessage,
|
||||
AlertResponse,
|
||||
AlertUpdateRequest,
|
||||
AlertFilters,
|
||||
AlertQueryParams,
|
||||
AlertDashboardData,
|
||||
NotificationSettings,
|
||||
ChannelRoutingConfig,
|
||||
WebhookConfig,
|
||||
WebhookPayload,
|
||||
AlertProcessingStatus,
|
||||
ProcessingMetrics,
|
||||
SSEAlertMessage,
|
||||
PaginatedResponse,
|
||||
} from '../types/alert_processor';
|
||||
|
||||
class AlertProcessorService {
|
||||
private readonly baseUrl = '/alerts';
|
||||
private readonly notificationUrl = '/notifications';
|
||||
private readonly webhookUrl = '/webhooks';
|
||||
|
||||
// Alert Management (these would be exposed via other services like inventory, production, etc.)
|
||||
async getAlerts(
|
||||
tenantId: string,
|
||||
queryParams?: AlertQueryParams
|
||||
): Promise<PaginatedResponse<AlertResponse>> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (queryParams?.severity?.length) {
|
||||
queryParams.severity.forEach(s => params.append('severity', s));
|
||||
}
|
||||
if (queryParams?.type?.length) {
|
||||
queryParams.type.forEach(t => params.append('type', t));
|
||||
}
|
||||
if (queryParams?.service?.length) {
|
||||
queryParams.service.forEach(s => params.append('service', s));
|
||||
}
|
||||
if (queryParams?.item_type?.length) {
|
||||
queryParams.item_type.forEach(it => params.append('item_type', it));
|
||||
}
|
||||
if (queryParams?.date_from) params.append('date_from', queryParams.date_from);
|
||||
if (queryParams?.date_to) params.append('date_to', queryParams.date_to);
|
||||
if (queryParams?.status) params.append('status', queryParams.status);
|
||||
if (queryParams?.search) params.append('search', queryParams.search);
|
||||
if (queryParams?.limit) params.append('limit', queryParams.limit.toString());
|
||||
if (queryParams?.offset) params.append('offset', queryParams.offset.toString());
|
||||
if (queryParams?.sort_by) params.append('sort_by', queryParams.sort_by);
|
||||
if (queryParams?.sort_order) params.append('sort_order', queryParams.sort_order);
|
||||
|
||||
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||||
return apiClient.get<PaginatedResponse<AlertResponse>>(
|
||||
`${this.baseUrl}/tenants/${tenantId}${queryString}`
|
||||
);
|
||||
}
|
||||
|
||||
async getAlert(tenantId: string, alertId: string): Promise<AlertResponse> {
|
||||
return apiClient.get<AlertResponse>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/${alertId}`
|
||||
);
|
||||
}
|
||||
|
||||
async updateAlert(
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
updateData: AlertUpdateRequest
|
||||
): Promise<AlertResponse> {
|
||||
return apiClient.put<AlertResponse>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/${alertId}`,
|
||||
updateData
|
||||
);
|
||||
}
|
||||
|
||||
async dismissAlert(tenantId: string, alertId: string): Promise<AlertResponse> {
|
||||
return apiClient.put<AlertResponse>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/${alertId}`,
|
||||
{ status: 'dismissed' }
|
||||
);
|
||||
}
|
||||
|
||||
async acknowledgeAlert(
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
notes?: string
|
||||
): Promise<AlertResponse> {
|
||||
return apiClient.put<AlertResponse>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/${alertId}`,
|
||||
{ status: 'acknowledged', notes }
|
||||
);
|
||||
}
|
||||
|
||||
async resolveAlert(
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
notes?: string
|
||||
): Promise<AlertResponse> {
|
||||
return apiClient.put<AlertResponse>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/${alertId}`,
|
||||
{ status: 'resolved', notes }
|
||||
);
|
||||
}
|
||||
|
||||
// Dashboard Data
|
||||
async getDashboardData(tenantId: string): Promise<AlertDashboardData> {
|
||||
return apiClient.get<AlertDashboardData>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/dashboard`
|
||||
);
|
||||
}
|
||||
|
||||
// Notification Settings
|
||||
async getNotificationSettings(tenantId: string): Promise<NotificationSettings> {
|
||||
return apiClient.get<NotificationSettings>(
|
||||
`${this.notificationUrl}/tenants/${tenantId}/settings`
|
||||
);
|
||||
}
|
||||
|
||||
async updateNotificationSettings(
|
||||
tenantId: string,
|
||||
settings: Partial<NotificationSettings>
|
||||
): Promise<NotificationSettings> {
|
||||
return apiClient.put<NotificationSettings>(
|
||||
`${this.notificationUrl}/tenants/${tenantId}/settings`,
|
||||
settings
|
||||
);
|
||||
}
|
||||
|
||||
async getChannelRoutingConfig(): Promise<ChannelRoutingConfig> {
|
||||
return apiClient.get<ChannelRoutingConfig>(`${this.notificationUrl}/routing-config`);
|
||||
}
|
||||
|
||||
// Webhook Management
|
||||
async getWebhooks(tenantId: string): Promise<WebhookConfig[]> {
|
||||
return apiClient.get<WebhookConfig[]>(`${this.webhookUrl}/tenants/${tenantId}`);
|
||||
}
|
||||
|
||||
async createWebhook(
|
||||
tenantId: string,
|
||||
webhook: Omit<WebhookConfig, 'tenant_id'>
|
||||
): Promise<WebhookConfig> {
|
||||
return apiClient.post<WebhookConfig>(
|
||||
`${this.webhookUrl}/tenants/${tenantId}`,
|
||||
{ ...webhook, tenant_id: tenantId }
|
||||
);
|
||||
}
|
||||
|
||||
async updateWebhook(
|
||||
tenantId: string,
|
||||
webhookId: string,
|
||||
webhook: Partial<WebhookConfig>
|
||||
): Promise<WebhookConfig> {
|
||||
return apiClient.put<WebhookConfig>(
|
||||
`${this.webhookUrl}/tenants/${tenantId}/${webhookId}`,
|
||||
webhook
|
||||
);
|
||||
}
|
||||
|
||||
async deleteWebhook(tenantId: string, webhookId: string): Promise<{ message: string }> {
|
||||
return apiClient.delete<{ message: string }>(
|
||||
`${this.webhookUrl}/tenants/${tenantId}/${webhookId}`
|
||||
);
|
||||
}
|
||||
|
||||
async testWebhook(
|
||||
tenantId: string,
|
||||
webhookId: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
return apiClient.post<{ success: boolean; message: string }>(
|
||||
`${this.webhookUrl}/tenants/${tenantId}/${webhookId}/test`
|
||||
);
|
||||
}
|
||||
|
||||
// Processing Status and Metrics
|
||||
async getProcessingStatus(
|
||||
tenantId: string,
|
||||
alertId: string
|
||||
): Promise<AlertProcessingStatus> {
|
||||
return apiClient.get<AlertProcessingStatus>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/${alertId}/processing-status`
|
||||
);
|
||||
}
|
||||
|
||||
async getProcessingMetrics(tenantId: string): Promise<ProcessingMetrics> {
|
||||
return apiClient.get<ProcessingMetrics>(
|
||||
`${this.baseUrl}/tenants/${tenantId}/processing-metrics`
|
||||
);
|
||||
}
|
||||
|
||||
// SSE (Server-Sent Events) connection helpers
|
||||
getSSEUrl(tenantId: string): string {
|
||||
const baseUrl = apiClient.getAxiosInstance().defaults.baseURL;
|
||||
return `${baseUrl}/sse/tenants/${tenantId}/alerts`;
|
||||
}
|
||||
|
||||
createSSEConnection(tenantId: string, token?: string): EventSource {
|
||||
const sseUrl = this.getSSEUrl(tenantId);
|
||||
const urlWithToken = token ? `${sseUrl}?token=${token}` : sseUrl;
|
||||
|
||||
return new EventSource(urlWithToken);
|
||||
}
|
||||
|
||||
// Utility methods for working with alerts
|
||||
static formatAlertMessage(alert: AlertMessage): string {
|
||||
return `[${alert.severity.toUpperCase()}] ${alert.title}: ${alert.message}`;
|
||||
}
|
||||
|
||||
static getAlertIcon(alert: AlertMessage): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
inventory_low: '📦',
|
||||
quality_issue: '⚠️',
|
||||
delivery_delay: '🚚',
|
||||
production_delay: '🏭',
|
||||
equipment_failure: '🔧',
|
||||
food_safety: '🦠',
|
||||
temperature_alert: '🌡️',
|
||||
expiry_warning: '⏰',
|
||||
forecast_accuracy: '📊',
|
||||
demand_spike: '📈',
|
||||
supplier_issue: '🏢',
|
||||
cost_optimization: '💰',
|
||||
revenue_opportunity: '💡',
|
||||
};
|
||||
return iconMap[alert.type] || '🔔';
|
||||
}
|
||||
|
||||
static getSeverityColor(severity: string): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
urgent: '#dc2626', // red-600
|
||||
high: '#ea580c', // orange-600
|
||||
medium: '#d97706', // amber-600
|
||||
low: '#65a30d', // lime-600
|
||||
};
|
||||
return colorMap[severity] || '#6b7280'; // gray-500
|
||||
}
|
||||
|
||||
// Message queuing helpers (for RabbitMQ integration)
|
||||
static createAlertMessage(
|
||||
tenantId: string,
|
||||
alert: Omit<AlertMessage, 'id' | 'tenant_id' | 'timestamp'>
|
||||
): AlertMessage {
|
||||
return {
|
||||
id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
tenant_id: tenantId,
|
||||
timestamp: new Date().toISOString(),
|
||||
...alert,
|
||||
};
|
||||
}
|
||||
|
||||
static validateWebhookSignature(
|
||||
payload: string,
|
||||
signature: string,
|
||||
secret: string
|
||||
): boolean {
|
||||
// This would typically use crypto.createHmac for HMAC-SHA256 verification
|
||||
// Implementation depends on the specific signature algorithm used
|
||||
const crypto = window.crypto || (window as any).msCrypto;
|
||||
if (!crypto?.subtle) {
|
||||
console.warn('WebCrypto API not available for signature verification');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simplified example - actual implementation would use proper HMAC verification
|
||||
return signature.length > 0 && secret.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export singleton instance
|
||||
export const alertProcessorService = new AlertProcessorService();
|
||||
export default alertProcessorService;
|
||||
62
frontend/src/api/services/distribution.ts
Normal file
62
frontend/src/api/services/distribution.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/services/distribution.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* Distribution Service - Complete backend alignment
|
||||
*
|
||||
* Backend API structure:
|
||||
* - services/distribution/app/api/routes.py
|
||||
* - services/distribution/app/api/shipments.py
|
||||
*
|
||||
* Last Updated: 2025-12-03
|
||||
* Status: ✅ Complete - Backend alignment
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export class DistributionService {
|
||||
private readonly baseUrl = '/tenants';
|
||||
|
||||
// ===================================================================
|
||||
// SHIPMENTS
|
||||
// Backend: services/distribution/app/api/shipments.py
|
||||
// ===================================================================
|
||||
|
||||
async getShipments(
|
||||
tenantId: string,
|
||||
date?: string
|
||||
): Promise<any[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (date) params.append('date', date);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${this.baseUrl}/${tenantId}/distribution/shipments${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await apiClient.get<any>(url);
|
||||
return response.shipments || response;
|
||||
}
|
||||
|
||||
async getShipment(
|
||||
tenantId: string,
|
||||
shipmentId: string
|
||||
): Promise<any> {
|
||||
return apiClient.get(`${this.baseUrl}/${tenantId}/distribution/shipments/${shipmentId}`);
|
||||
}
|
||||
|
||||
async getRouteSequences(
|
||||
tenantId: string,
|
||||
date?: string
|
||||
): Promise<any[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (date) params.append('date', date);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${this.baseUrl}/${tenantId}/distribution/routes${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await apiClient.get<any>(url);
|
||||
return response.routes || response;
|
||||
}
|
||||
}
|
||||
|
||||
export const distributionService = new DistributionService();
|
||||
export default distributionService;
|
||||
@@ -1,104 +0,0 @@
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export interface NetworkSummary {
|
||||
parent_tenant_id: string;
|
||||
total_tenants: number;
|
||||
child_tenant_count: number;
|
||||
total_revenue: number;
|
||||
network_sales_30d: number;
|
||||
active_alerts: number;
|
||||
efficiency_score: number;
|
||||
growth_rate: number;
|
||||
production_volume_30d: number;
|
||||
pending_internal_transfers_count: number;
|
||||
active_shipments_count: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
export interface ChildPerformance {
|
||||
rankings: Array<{
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
anonymized_name: string;
|
||||
metric_value: number;
|
||||
rank: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DistributionOverview {
|
||||
route_sequences: any[];
|
||||
status_counts: {
|
||||
pending: number;
|
||||
in_transit: number;
|
||||
delivered: number;
|
||||
failed: number;
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ForecastSummary {
|
||||
aggregated_forecasts: Record<string, any>;
|
||||
days_forecast: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
export interface NetworkPerformance {
|
||||
metrics: Record<string, any>;
|
||||
}
|
||||
|
||||
export class EnterpriseService {
|
||||
private readonly baseUrl = '/tenants';
|
||||
|
||||
async getNetworkSummary(tenantId: string): Promise<NetworkSummary> {
|
||||
return apiClient.get<NetworkSummary>(`${this.baseUrl}/${tenantId}/enterprise/network-summary`);
|
||||
}
|
||||
|
||||
async getChildrenPerformance(
|
||||
tenantId: string,
|
||||
metric: string = 'sales',
|
||||
periodDays: number = 30
|
||||
): Promise<ChildPerformance> {
|
||||
const queryParams = new URLSearchParams({
|
||||
metric,
|
||||
period_days: periodDays.toString()
|
||||
});
|
||||
return apiClient.get<ChildPerformance>(
|
||||
`${this.baseUrl}/${tenantId}/enterprise/children-performance?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
async getDistributionOverview(tenantId: string, targetDate?: string): Promise<DistributionOverview> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (targetDate) {
|
||||
queryParams.append('target_date', targetDate);
|
||||
}
|
||||
return apiClient.get<DistributionOverview>(
|
||||
`${this.baseUrl}/${tenantId}/enterprise/distribution-overview?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
async getForecastSummary(tenantId: string, daysAhead: number = 7): Promise<ForecastSummary> {
|
||||
const queryParams = new URLSearchParams({
|
||||
days_ahead: daysAhead.toString()
|
||||
});
|
||||
return apiClient.get<ForecastSummary>(
|
||||
`${this.baseUrl}/${tenantId}/enterprise/forecast-summary?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
async getNetworkPerformance(
|
||||
tenantId: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<NetworkPerformance> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (startDate) queryParams.append('start_date', startDate);
|
||||
if (endDate) queryParams.append('end_date', endDate);
|
||||
|
||||
return apiClient.get<NetworkPerformance>(
|
||||
`${this.baseUrl}/${tenantId}/enterprise/network-performance?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const enterpriseService = new EnterpriseService();
|
||||
@@ -19,20 +19,18 @@ class ExternalDataService {
|
||||
* List all supported cities
|
||||
*/
|
||||
async listCities(): Promise<CityInfoResponse[]> {
|
||||
const response = await apiClient.get<CityInfoResponse[]>(
|
||||
return await apiClient.get<CityInfoResponse[]>(
|
||||
'/api/v1/external/cities'
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data availability for a specific city
|
||||
*/
|
||||
async getCityAvailability(cityId: string): Promise<DataAvailabilityResponse> {
|
||||
const response = await apiClient.get<DataAvailabilityResponse>(
|
||||
return await apiClient.get<DataAvailabilityResponse>(
|
||||
`/api/v1/external/operations/cities/${cityId}/availability`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,11 +45,10 @@ class ExternalDataService {
|
||||
end_date: string;
|
||||
}
|
||||
): Promise<WeatherDataResponse[]> {
|
||||
const response = await apiClient.get<WeatherDataResponse[]>(
|
||||
return await apiClient.get<WeatherDataResponse[]>(
|
||||
`/api/v1/tenants/${tenantId}/external/operations/historical-weather-optimized`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,11 +63,10 @@ class ExternalDataService {
|
||||
end_date: string;
|
||||
}
|
||||
): Promise<TrafficDataResponse[]> {
|
||||
const response = await apiClient.get<TrafficDataResponse[]>(
|
||||
return await apiClient.get<TrafficDataResponse[]>(
|
||||
`/api/v1/tenants/${tenantId}/external/operations/historical-traffic-optimized`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,11 +79,10 @@ class ExternalDataService {
|
||||
longitude: number;
|
||||
}
|
||||
): Promise<WeatherDataResponse> {
|
||||
const response = await apiClient.get<WeatherDataResponse>(
|
||||
return await apiClient.get<WeatherDataResponse>(
|
||||
`/api/v1/tenants/${tenantId}/external/operations/weather/current`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,11 +96,10 @@ class ExternalDataService {
|
||||
days?: number;
|
||||
}
|
||||
): Promise<WeatherDataResponse[]> {
|
||||
const response = await apiClient.get<WeatherDataResponse[]>(
|
||||
return await apiClient.get<WeatherDataResponse[]>(
|
||||
`/api/v1/tenants/${tenantId}/external/operations/weather/forecast`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,11 +112,10 @@ class ExternalDataService {
|
||||
longitude: number;
|
||||
}
|
||||
): Promise<TrafficDataResponse> {
|
||||
const response = await apiClient.get<TrafficDataResponse>(
|
||||
return await apiClient.get<TrafficDataResponse>(
|
||||
`/api/v1/tenants/${tenantId}/external/operations/traffic/current`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -393,6 +393,20 @@ export class InventoryService {
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Batch Inventory Summary (Enterprise Feature)
|
||||
// Backend: services/inventory/app/api/inventory_operations.py
|
||||
// ===================================================================
|
||||
|
||||
async getBatchInventorySummary(tenantIds: string[]): Promise<Record<string, any>> {
|
||||
return apiClient.post<Record<string, any>>(
|
||||
'/tenants/batch/inventory-summary',
|
||||
{
|
||||
tenant_ids: tenantIds,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Food Safety
|
||||
// Backend: services/inventory/app/api/food_safety_operations.py
|
||||
|
||||
@@ -43,7 +43,7 @@ class NominatimService {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<NominatimResult[]>(`${this.baseUrl}/search`, {
|
||||
return await apiClient.get<NominatimResult[]>(`${this.baseUrl}/search`, {
|
||||
params: {
|
||||
q: query,
|
||||
format: 'json',
|
||||
@@ -52,8 +52,6 @@ class NominatimService {
|
||||
countrycodes: 'es', // Spain only
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Address search failed:', error);
|
||||
return [];
|
||||
|
||||
@@ -9,97 +9,26 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
import {
|
||||
OrchestratorWorkflowRequest,
|
||||
OrchestratorWorkflowResponse,
|
||||
WorkflowExecutionSummary,
|
||||
WorkflowExecutionDetail,
|
||||
OrchestratorStatus,
|
||||
OrchestratorConfig,
|
||||
WorkflowStepResult
|
||||
} from '../types/orchestrator';
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR WORKFLOW TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface OrchestratorWorkflowRequest {
|
||||
target_date?: string; // YYYY-MM-DD, defaults to tomorrow
|
||||
planning_horizon_days?: number; // Default: 14
|
||||
|
||||
// Forecasting options
|
||||
forecast_days_ahead?: number; // Default: 7
|
||||
|
||||
// Production options
|
||||
auto_schedule_production?: boolean; // Default: true
|
||||
production_planning_days?: number; // Default: 1
|
||||
|
||||
// Procurement options
|
||||
auto_create_purchase_orders?: boolean; // Default: true
|
||||
auto_approve_purchase_orders?: boolean; // Default: false
|
||||
safety_stock_percentage?: number; // Default: 20.00
|
||||
|
||||
// Orchestrator options
|
||||
skip_on_error?: boolean; // Continue to next step if one fails
|
||||
notify_on_completion?: boolean; // Send notification when done
|
||||
}
|
||||
|
||||
export interface WorkflowStepResult {
|
||||
step: 'forecasting' | 'production' | 'procurement';
|
||||
status: 'success' | 'failed' | 'skipped';
|
||||
duration_ms: number;
|
||||
data?: any;
|
||||
error?: string;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface OrchestratorWorkflowResponse {
|
||||
success: boolean;
|
||||
workflow_id: string;
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
execution_date: string;
|
||||
total_duration_ms: number;
|
||||
|
||||
steps: WorkflowStepResult[];
|
||||
|
||||
// Step-specific results
|
||||
forecast_result?: {
|
||||
forecast_id: string;
|
||||
total_forecasts: number;
|
||||
forecast_data: any;
|
||||
};
|
||||
|
||||
production_result?: {
|
||||
schedule_id: string;
|
||||
total_batches: number;
|
||||
total_quantity: number;
|
||||
};
|
||||
|
||||
procurement_result?: {
|
||||
plan_id: string;
|
||||
total_requirements: number;
|
||||
total_cost: string;
|
||||
purchase_orders_created: number;
|
||||
purchase_orders_auto_approved: number;
|
||||
};
|
||||
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionSummary {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
started_at: string;
|
||||
completed_at?: string;
|
||||
total_duration_ms?: number;
|
||||
steps_completed: number;
|
||||
steps_total: number;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionDetail extends WorkflowExecutionSummary {
|
||||
steps: WorkflowStepResult[];
|
||||
forecast_id?: string;
|
||||
production_schedule_id?: string;
|
||||
procurement_plan_id?: string;
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
// Re-export types for backward compatibility
|
||||
export type {
|
||||
OrchestratorWorkflowRequest,
|
||||
OrchestratorWorkflowResponse,
|
||||
WorkflowExecutionSummary,
|
||||
WorkflowExecutionDetail,
|
||||
OrchestratorStatus,
|
||||
OrchestratorConfig,
|
||||
WorkflowStepResult
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR WORKFLOW API FUNCTIONS
|
||||
@@ -230,21 +159,6 @@ export async function retryWorkflowExecution(
|
||||
// ORCHESTRATOR STATUS & HEALTH
|
||||
// ============================================================================
|
||||
|
||||
export interface OrchestratorStatus {
|
||||
is_leader: boolean;
|
||||
scheduler_running: boolean;
|
||||
next_scheduled_run?: string;
|
||||
last_execution?: {
|
||||
id: string;
|
||||
target_date: string;
|
||||
status: string;
|
||||
completed_at: string;
|
||||
};
|
||||
total_executions_today: number;
|
||||
total_successful_executions: number;
|
||||
total_failed_executions: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestrator service status
|
||||
*/
|
||||
@@ -256,22 +170,21 @@ export async function getOrchestratorStatus(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp of last orchestration run
|
||||
*/
|
||||
export async function getLastOrchestrationRun(
|
||||
tenantId: string
|
||||
): Promise<{ timestamp: string | null; runNumber: number | null }> {
|
||||
return apiClient.get<{ timestamp: string | null; runNumber: number | null }>(
|
||||
`/tenants/${tenantId}/orchestrator/last-run`
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
export interface OrchestratorConfig {
|
||||
enabled: boolean;
|
||||
schedule_cron: string; // Cron expression for daily run
|
||||
default_planning_horizon_days: number;
|
||||
auto_create_purchase_orders: boolean;
|
||||
auto_approve_purchase_orders: boolean;
|
||||
safety_stock_percentage: number;
|
||||
notify_on_completion: boolean;
|
||||
notify_on_failure: boolean;
|
||||
skip_on_error: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestrator configuration for tenant
|
||||
*/
|
||||
|
||||
@@ -445,6 +445,24 @@ export class ProcurementService {
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expected deliveries
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/expected-deliveries
|
||||
*/
|
||||
static async getExpectedDeliveries(
|
||||
tenantId: string,
|
||||
params?: { days_ahead?: number; include_overdue?: boolean }
|
||||
): Promise<{ deliveries: any[]; total_count: number }> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.days_ahead !== undefined) queryParams.append('days_ahead', params.days_ahead.toString());
|
||||
if (params?.include_overdue !== undefined) queryParams.append('include_overdue', params.include_overdue.toString());
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = `/tenants/${tenantId}/procurement/expected-deliveries${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return apiClient.get<{ deliveries: any[]; total_count: number }>(url);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProcurementService;
|
||||
|
||||
@@ -118,6 +118,7 @@ export class ProductionService {
|
||||
return apiClient.get<BatchStatistics>(url);
|
||||
}
|
||||
|
||||
|
||||
// ===================================================================
|
||||
// ATOMIC: Production Schedules CRUD
|
||||
// Backend: services/production/app/api/production_schedules.py
|
||||
@@ -406,6 +407,20 @@ export class ProductionService {
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ANALYTICS: Batch Production Summary (Enterprise Feature)
|
||||
// Backend: services/production/app/api/analytics.py
|
||||
// ===================================================================
|
||||
|
||||
async getBatchProductionSummary(tenantIds: string[]): Promise<Record<string, any>> {
|
||||
return apiClient.post<Record<string, any>>(
|
||||
'/tenants/batch/production-summary',
|
||||
{
|
||||
tenant_ids: tenantIds,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Scheduler
|
||||
// ===================================================================
|
||||
|
||||
@@ -196,6 +196,26 @@ export class SalesService {
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Batch Sales Summary (Enterprise Feature)
|
||||
// Backend: services/sales/app/api/sales_operations.py
|
||||
// ===================================================================
|
||||
|
||||
async getBatchSalesSummary(
|
||||
tenantIds: string[],
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): Promise<Record<string, any>> {
|
||||
return apiClient.post<Record<string, any>>(
|
||||
'/tenants/batch/sales-summary',
|
||||
{
|
||||
tenant_ids: tenantIds,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Aggregation
|
||||
// Backend: services/sales/app/api/sales_operations.py
|
||||
|
||||
@@ -380,6 +380,8 @@ export class SubscriptionService {
|
||||
is_read_only: boolean;
|
||||
cancellation_effective_date: string | null;
|
||||
days_until_inactive: number | null;
|
||||
billing_cycle?: string;
|
||||
next_billing_date?: string;
|
||||
}> {
|
||||
return apiClient.get(`/subscriptions/${tenantId}/status`);
|
||||
}
|
||||
@@ -483,10 +485,10 @@ export class SubscriptionService {
|
||||
|
||||
return {
|
||||
tier: status.plan as SubscriptionTier,
|
||||
billing_cycle: 'monthly', // TODO: Get from actual subscription data
|
||||
billing_cycle: (status.billing_cycle as 'monthly' | 'yearly') || 'monthly',
|
||||
monthly_price: currentPlan?.monthly_price || 0,
|
||||
yearly_price: currentPlan?.yearly_price || 0,
|
||||
renewal_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // TODO: Get from actual subscription
|
||||
renewal_date: status.next_billing_date || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
limits: {
|
||||
users: currentPlan?.limits?.users ?? null,
|
||||
locations: currentPlan?.limits?.locations ?? null,
|
||||
|
||||
@@ -78,6 +78,14 @@ export class TenantService {
|
||||
return apiClient.get<TenantAccessResponse>(`${this.baseUrl}/${tenantId}/my-access`);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Enterprise Hierarchy
|
||||
// Backend: services/tenant/app/api/tenant_hierarchy.py
|
||||
// ===================================================================
|
||||
async getChildTenants(parentTenantId: string): Promise<TenantResponse[]> {
|
||||
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/${parentTenantId}/children`);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Search & Discovery
|
||||
// Backend: services/tenant/app/api/tenant_operations.py
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
/**
|
||||
* Alert Processor service TypeScript type definitions
|
||||
* Mirrored from backend alert processing schemas
|
||||
*/
|
||||
|
||||
// Enums
|
||||
export enum AlertItemType {
|
||||
ALERT = 'alert',
|
||||
RECOMMENDATION = 'recommendation',
|
||||
}
|
||||
|
||||
export enum AlertType {
|
||||
INVENTORY_LOW = 'inventory_low',
|
||||
QUALITY_ISSUE = 'quality_issue',
|
||||
DELIVERY_DELAY = 'delivery_delay',
|
||||
PRODUCTION_DELAY = 'production_delay',
|
||||
EQUIPMENT_FAILURE = 'equipment_failure',
|
||||
FOOD_SAFETY = 'food_safety',
|
||||
TEMPERATURE_ALERT = 'temperature_alert',
|
||||
EXPIRY_WARNING = 'expiry_warning',
|
||||
FORECAST_ACCURACY = 'forecast_accuracy',
|
||||
DEMAND_SPIKE = 'demand_spike',
|
||||
SUPPLIER_ISSUE = 'supplier_issue',
|
||||
COST_OPTIMIZATION = 'cost_optimization',
|
||||
REVENUE_OPPORTUNITY = 'revenue_opportunity',
|
||||
}
|
||||
|
||||
export enum AlertSeverity {
|
||||
URGENT = 'urgent',
|
||||
HIGH = 'high',
|
||||
MEDIUM = 'medium',
|
||||
LOW = 'low',
|
||||
}
|
||||
|
||||
export enum AlertService {
|
||||
INVENTORY = 'inventory',
|
||||
PRODUCTION = 'production',
|
||||
SUPPLIERS = 'suppliers',
|
||||
FORECASTING = 'forecasting',
|
||||
QUALITY = 'quality',
|
||||
FINANCE = 'finance',
|
||||
OPERATIONS = 'operations',
|
||||
}
|
||||
|
||||
export enum NotificationChannel {
|
||||
WHATSAPP = 'whatsapp',
|
||||
EMAIL = 'email',
|
||||
PUSH = 'push',
|
||||
DASHBOARD = 'dashboard',
|
||||
SMS = 'sms',
|
||||
}
|
||||
|
||||
// Core alert data structures
|
||||
export interface AlertAction {
|
||||
action: string;
|
||||
label: string;
|
||||
endpoint?: string;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
payload?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AlertMessage {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
item_type: AlertItemType;
|
||||
type: AlertType;
|
||||
severity: AlertSeverity;
|
||||
service: AlertService;
|
||||
title: string;
|
||||
message: string;
|
||||
actions: AlertAction[];
|
||||
metadata: Record<string, any>;
|
||||
timestamp: string; // ISO 8601 date string
|
||||
}
|
||||
|
||||
// Channel routing configuration
|
||||
export interface ChannelRoutingConfig {
|
||||
urgent: NotificationChannel[];
|
||||
high: NotificationChannel[];
|
||||
medium: NotificationChannel[];
|
||||
low: NotificationChannel[];
|
||||
recommendations: NotificationChannel[];
|
||||
}
|
||||
|
||||
export interface BusinessHours {
|
||||
start_hour: number; // 0-23
|
||||
end_hour: number; // 0-23
|
||||
days: number[]; // 0-6, Sunday=0
|
||||
timezone?: string; // e.g., 'Europe/Madrid'
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
tenant_id: string;
|
||||
channels_enabled: NotificationChannel[];
|
||||
business_hours: BusinessHours;
|
||||
emergency_contacts: {
|
||||
whatsapp?: string;
|
||||
email?: string;
|
||||
sms?: string;
|
||||
};
|
||||
channel_preferences: {
|
||||
[key in AlertSeverity]?: NotificationChannel[];
|
||||
};
|
||||
}
|
||||
|
||||
// Processing status and metrics
|
||||
export interface ProcessingMetrics {
|
||||
total_processed: number;
|
||||
successful: number;
|
||||
failed: number;
|
||||
retries: number;
|
||||
average_processing_time_ms: number;
|
||||
}
|
||||
|
||||
export interface AlertProcessingStatus {
|
||||
alert_id: string;
|
||||
tenant_id: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed' | 'retrying';
|
||||
created_at: string;
|
||||
processed_at?: string;
|
||||
error_message?: string;
|
||||
retry_count: number;
|
||||
channels_sent: NotificationChannel[];
|
||||
delivery_status: {
|
||||
[channel in NotificationChannel]?: {
|
||||
status: 'pending' | 'sent' | 'delivered' | 'failed';
|
||||
sent_at?: string;
|
||||
delivered_at?: string;
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Queue message structures (for RabbitMQ integration)
|
||||
export interface QueueMessage {
|
||||
id: string;
|
||||
routing_key: string;
|
||||
exchange: string;
|
||||
payload: AlertMessage;
|
||||
headers?: Record<string, any>;
|
||||
properties?: {
|
||||
delivery_mode?: number;
|
||||
priority?: number;
|
||||
correlation_id?: string;
|
||||
reply_to?: string;
|
||||
expiration?: string;
|
||||
message_id?: string;
|
||||
timestamp?: number;
|
||||
type?: string;
|
||||
user_id?: string;
|
||||
app_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// SSE (Server-Sent Events) message types for real-time updates
|
||||
export interface SSEAlertMessage {
|
||||
type: 'alert' | 'recommendation' | 'alert_update' | 'system_status';
|
||||
data: AlertMessage | AlertProcessingStatus | SystemStatusMessage;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface SystemStatusMessage {
|
||||
service: 'alert_processor';
|
||||
status: 'healthy' | 'degraded' | 'down';
|
||||
message?: string;
|
||||
metrics: ProcessingMetrics;
|
||||
}
|
||||
|
||||
// Dashboard integration types
|
||||
export interface AlertDashboardData {
|
||||
active_alerts: AlertMessage[];
|
||||
recent_recommendations: AlertMessage[];
|
||||
severity_counts: {
|
||||
[key in AlertSeverity]: number;
|
||||
};
|
||||
service_breakdown: {
|
||||
[key in AlertService]: number;
|
||||
};
|
||||
processing_stats: ProcessingMetrics;
|
||||
}
|
||||
|
||||
export interface AlertFilters {
|
||||
severity?: AlertSeverity[];
|
||||
type?: AlertType[];
|
||||
service?: AlertService[];
|
||||
item_type?: AlertItemType[];
|
||||
date_from?: string; // ISO 8601 date string
|
||||
date_to?: string; // ISO 8601 date string
|
||||
status?: 'active' | 'acknowledged' | 'resolved' | 'dismissed';
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface AlertQueryParams extends AlertFilters {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort_by?: 'timestamp' | 'severity' | 'type';
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// Alert lifecycle management
|
||||
export interface AlertUpdateRequest {
|
||||
status?: 'acknowledged' | 'resolved' | 'dismissed';
|
||||
notes?: string;
|
||||
assigned_to?: string;
|
||||
priority_override?: AlertSeverity;
|
||||
}
|
||||
|
||||
export interface AlertResponse extends AlertMessage {
|
||||
status: 'active' | 'acknowledged' | 'resolved' | 'dismissed';
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
acknowledged_at?: string;
|
||||
acknowledged_by?: string;
|
||||
resolved_at?: string;
|
||||
resolved_by?: string;
|
||||
notes?: string;
|
||||
assigned_to?: string;
|
||||
}
|
||||
|
||||
// Webhook integration for external systems
|
||||
export interface WebhookConfig {
|
||||
tenant_id: string;
|
||||
webhook_url: string;
|
||||
secret_token: string;
|
||||
enabled: boolean;
|
||||
event_types: (AlertType | 'all')[];
|
||||
severity_filter: AlertSeverity[];
|
||||
headers?: Record<string, string>;
|
||||
retry_config: {
|
||||
max_retries: number;
|
||||
retry_delay_ms: number;
|
||||
backoff_multiplier: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WebhookPayload {
|
||||
event_type: 'alert_created' | 'alert_updated' | 'alert_resolved';
|
||||
alert: AlertResponse;
|
||||
tenant_id: string;
|
||||
webhook_id: string;
|
||||
timestamp: string;
|
||||
signature: string; // HMAC signature for verification
|
||||
}
|
||||
|
||||
// API Response Wrappers
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
// Export all types
|
||||
export type {
|
||||
// Add any additional export aliases if needed
|
||||
};
|
||||
457
frontend/src/api/types/events.ts
Normal file
457
frontend/src/api/types/events.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* Unified Event Type System - Single Source of Truth
|
||||
*
|
||||
* Complete rewrite matching backend response structure exactly.
|
||||
* NO backward compatibility, NO legacy fields.
|
||||
*
|
||||
* Backend files this mirrors:
|
||||
* - /services/alert_processor/app/models/events_clean.py
|
||||
* - /services/alert_processor/app/models/response_models_clean.py
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// ENUMS - Matching Backend Exactly
|
||||
// ============================================================
|
||||
|
||||
export enum EventClass {
|
||||
ALERT = 'alert',
|
||||
NOTIFICATION = 'notification',
|
||||
RECOMMENDATION = 'recommendation',
|
||||
}
|
||||
|
||||
export enum AlertTypeClass {
|
||||
ACTION_NEEDED = 'action_needed',
|
||||
PREVENTED_ISSUE = 'prevented_issue',
|
||||
TREND_WARNING = 'trend_warning',
|
||||
ESCALATION = 'escalation',
|
||||
INFORMATION = 'information',
|
||||
}
|
||||
|
||||
export enum PriorityLevel {
|
||||
CRITICAL = 'critical', // 90-100
|
||||
IMPORTANT = 'important', // 70-89
|
||||
STANDARD = 'standard', // 50-69
|
||||
INFO = 'info', // 0-49
|
||||
}
|
||||
|
||||
export enum AlertStatus {
|
||||
ACTIVE = 'active',
|
||||
RESOLVED = 'resolved',
|
||||
ACKNOWLEDGED = 'acknowledged',
|
||||
IN_PROGRESS = 'in_progress',
|
||||
DISMISSED = 'dismissed',
|
||||
}
|
||||
|
||||
export enum SmartActionType {
|
||||
APPROVE_PO = 'approve_po',
|
||||
REJECT_PO = 'reject_po',
|
||||
MODIFY_PO = 'modify_po',
|
||||
VIEW_PO_DETAILS = 'view_po_details',
|
||||
CALL_SUPPLIER = 'call_supplier',
|
||||
NAVIGATE = 'navigate',
|
||||
ADJUST_PRODUCTION = 'adjust_production',
|
||||
START_PRODUCTION_BATCH = 'start_production_batch',
|
||||
NOTIFY_CUSTOMER = 'notify_customer',
|
||||
CANCEL_AUTO_ACTION = 'cancel_auto_action',
|
||||
MARK_DELIVERY_RECEIVED = 'mark_delivery_received',
|
||||
COMPLETE_STOCK_RECEIPT = 'complete_stock_receipt',
|
||||
OPEN_REASONING = 'open_reasoning',
|
||||
SNOOZE = 'snooze',
|
||||
DISMISS = 'dismiss',
|
||||
MARK_READ = 'mark_read',
|
||||
}
|
||||
|
||||
export enum NotificationType {
|
||||
STATE_CHANGE = 'state_change',
|
||||
COMPLETION = 'completion',
|
||||
ARRIVAL = 'arrival',
|
||||
DEPARTURE = 'departure',
|
||||
UPDATE = 'update',
|
||||
SYSTEM_EVENT = 'system_event',
|
||||
}
|
||||
|
||||
export enum RecommendationType {
|
||||
OPTIMIZATION = 'optimization',
|
||||
COST_REDUCTION = 'cost_reduction',
|
||||
RISK_MITIGATION = 'risk_mitigation',
|
||||
TREND_INSIGHT = 'trend_insight',
|
||||
BEST_PRACTICE = 'best_practice',
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CONTEXT INTERFACES - Matching Backend Response Models
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* i18n display context with parameterized content
|
||||
* Backend field name: "i18n" (NOT "display")
|
||||
*/
|
||||
export interface I18nDisplayContext {
|
||||
title_key: string;
|
||||
message_key: string;
|
||||
title_params: Record<string, any>;
|
||||
message_params: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface BusinessImpactContext {
|
||||
financial_impact_eur?: number;
|
||||
waste_prevented_eur?: number;
|
||||
time_saved_minutes?: number;
|
||||
production_loss_avoided_eur?: number;
|
||||
potential_loss_eur?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Urgency context
|
||||
* Backend field name: "urgency" (NOT "urgency_context")
|
||||
*/
|
||||
export interface UrgencyContext {
|
||||
deadline_utc?: string; // ISO date string
|
||||
hours_until_consequence?: number;
|
||||
auto_action_countdown_seconds?: number;
|
||||
auto_action_cancelled?: boolean;
|
||||
urgency_reason_key?: string; // i18n key
|
||||
urgency_reason_params?: Record<string, any>;
|
||||
priority: string; // "critical", "urgent", "normal", "info"
|
||||
}
|
||||
|
||||
export interface UserAgencyContext {
|
||||
action_required: boolean;
|
||||
external_party_required?: boolean;
|
||||
external_party_name?: string;
|
||||
external_party_contact?: string;
|
||||
estimated_resolution_time_minutes?: number;
|
||||
user_control_level: string; // "full", "partial", "none"
|
||||
action_urgency: string; // "immediate", "soon", "normal"
|
||||
}
|
||||
|
||||
export interface TrendContext {
|
||||
metric_name: string;
|
||||
current_value: number;
|
||||
baseline_value: number;
|
||||
change_percentage: number;
|
||||
direction: 'increasing' | 'decreasing';
|
||||
significance: 'high' | 'medium' | 'low';
|
||||
period_days: number;
|
||||
possible_causes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart action with parameterized i18n labels
|
||||
* Backend field name in Alert: "smart_actions" (NOT "actions")
|
||||
*/
|
||||
export interface SmartAction {
|
||||
action_type: string;
|
||||
label_key: string; // i18n key for button text
|
||||
label_params?: Record<string, any>;
|
||||
variant: 'primary' | 'secondary' | 'danger' | 'ghost';
|
||||
disabled: boolean;
|
||||
consequence_key?: string; // i18n key for consequence text
|
||||
consequence_params?: Record<string, any>;
|
||||
disabled_reason?: string;
|
||||
disabled_reason_key?: string; // i18n key for disabled reason
|
||||
disabled_reason_params?: Record<string, any>;
|
||||
estimated_time_minutes?: number;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AIReasoningContext {
|
||||
summary_key?: string; // i18n key
|
||||
summary_params?: Record<string, any>;
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// EVENT RESPONSE TYPES - Base and Specific Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Base Event interface with common fields
|
||||
*/
|
||||
export interface Event {
|
||||
// Core Identity
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
event_class: EventClass;
|
||||
event_domain: string;
|
||||
event_type: string;
|
||||
service: string;
|
||||
|
||||
// i18n Display Context
|
||||
// CRITICAL: Backend uses "i18n", NOT "display"
|
||||
i18n: I18nDisplayContext;
|
||||
|
||||
// Classification
|
||||
priority_level: PriorityLevel;
|
||||
status: string;
|
||||
|
||||
// Timestamps
|
||||
created_at: string; // ISO date string
|
||||
updated_at: string; // ISO date string
|
||||
|
||||
// Optional context fields
|
||||
event_metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert - Full enrichment, lifecycle tracking
|
||||
*/
|
||||
export interface Alert extends Event {
|
||||
event_class: EventClass.ALERT;
|
||||
status: AlertStatus | string;
|
||||
|
||||
// Alert-specific classification
|
||||
type_class: AlertTypeClass;
|
||||
priority_score: number; // 0-100
|
||||
|
||||
// Rich Context
|
||||
// CRITICAL: Backend uses "urgency", NOT "urgency_context"
|
||||
business_impact?: BusinessImpactContext;
|
||||
urgency?: UrgencyContext;
|
||||
user_agency?: UserAgencyContext;
|
||||
trend_context?: TrendContext;
|
||||
orchestrator_context?: Record<string, any>;
|
||||
|
||||
// AI Intelligence
|
||||
ai_reasoning?: AIReasoningContext;
|
||||
confidence_score: number;
|
||||
|
||||
// Actions
|
||||
// CRITICAL: Backend uses "smart_actions", NOT "actions"
|
||||
smart_actions: SmartAction[];
|
||||
|
||||
// Entity References
|
||||
// CRITICAL: Backend uses "entity_links", NOT "entity_refs"
|
||||
entity_links: Record<string, string>;
|
||||
|
||||
// Timing Intelligence
|
||||
timing_decision?: string;
|
||||
scheduled_send_time?: string; // ISO date string
|
||||
|
||||
// Placement
|
||||
placement_hints?: string[];
|
||||
|
||||
// Escalation & Chaining
|
||||
action_created_at?: string; // ISO date string
|
||||
superseded_by_action_id?: string;
|
||||
hidden_from_ui?: boolean;
|
||||
|
||||
// Lifecycle
|
||||
resolved_at?: string; // ISO date string
|
||||
acknowledged_at?: string; // ISO date string
|
||||
acknowledged_by?: string;
|
||||
resolved_by?: string;
|
||||
notes?: string;
|
||||
assigned_to?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification - Lightweight, ephemeral (7-day TTL)
|
||||
*/
|
||||
export interface Notification extends Event {
|
||||
event_class: EventClass.NOTIFICATION;
|
||||
|
||||
// Notification-specific
|
||||
notification_type: NotificationType;
|
||||
|
||||
// Entity Context (lightweight)
|
||||
entity_type?: string; // 'batch', 'delivery', 'po', etc.
|
||||
entity_id?: string;
|
||||
old_state?: string;
|
||||
new_state?: string;
|
||||
|
||||
// Placement
|
||||
placement_hints?: string[];
|
||||
|
||||
// TTL
|
||||
expires_at?: string; // ISO date string
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommendation - Medium weight, dismissible
|
||||
*/
|
||||
export interface Recommendation extends Event {
|
||||
event_class: EventClass.RECOMMENDATION;
|
||||
|
||||
// Recommendation-specific
|
||||
recommendation_type: RecommendationType;
|
||||
|
||||
// Context (lighter than alerts)
|
||||
estimated_impact?: Record<string, any>;
|
||||
suggested_actions?: SmartAction[];
|
||||
|
||||
// AI Intelligence
|
||||
ai_reasoning?: AIReasoningContext;
|
||||
confidence_score?: number;
|
||||
|
||||
// Dismissal
|
||||
dismissed_at?: string; // ISO date string
|
||||
dismissed_by?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type for all event responses
|
||||
*/
|
||||
export type EventResponse = Alert | Notification | Recommendation;
|
||||
|
||||
// ============================================================
|
||||
// API RESPONSE WRAPPERS
|
||||
// ============================================================
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
}
|
||||
|
||||
export interface EventsSummary {
|
||||
total_count: number;
|
||||
active_count: number;
|
||||
critical_count: number;
|
||||
high_count: number;
|
||||
medium_count: number;
|
||||
low_count: number;
|
||||
resolved_count: number;
|
||||
acknowledged_count: number;
|
||||
}
|
||||
|
||||
export interface EventQueryParams {
|
||||
priority_level?: PriorityLevel | string;
|
||||
status?: AlertStatus | string;
|
||||
resolved?: boolean;
|
||||
event_class?: EventClass | string;
|
||||
event_domain?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TYPE GUARDS
|
||||
// ============================================================
|
||||
|
||||
export function isAlert(event: EventResponse | Event): event is Alert {
|
||||
return event.event_class === EventClass.ALERT || event.event_class === 'alert';
|
||||
}
|
||||
|
||||
export function isNotification(event: EventResponse | Event): event is Notification {
|
||||
return event.event_class === EventClass.NOTIFICATION || event.event_class === 'notification';
|
||||
}
|
||||
|
||||
export function isRecommendation(event: EventResponse | Event): event is Recommendation {
|
||||
return event.event_class === EventClass.RECOMMENDATION || event.event_class === 'recommendation';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
export function getPriorityColor(level: PriorityLevel | string): string {
|
||||
switch (level) {
|
||||
case PriorityLevel.CRITICAL:
|
||||
case 'critical':
|
||||
return 'var(--color-error)';
|
||||
case PriorityLevel.IMPORTANT:
|
||||
case 'important':
|
||||
return 'var(--color-warning)';
|
||||
case PriorityLevel.STANDARD:
|
||||
case 'standard':
|
||||
return 'var(--color-info)';
|
||||
case PriorityLevel.INFO:
|
||||
case 'info':
|
||||
return 'var(--color-success)';
|
||||
default:
|
||||
return 'var(--color-info)';
|
||||
}
|
||||
}
|
||||
|
||||
export function getPriorityIcon(level: PriorityLevel | string): string {
|
||||
switch (level) {
|
||||
case PriorityLevel.CRITICAL:
|
||||
case 'critical':
|
||||
return 'alert-triangle';
|
||||
case PriorityLevel.IMPORTANT:
|
||||
case 'important':
|
||||
return 'alert-circle';
|
||||
case PriorityLevel.STANDARD:
|
||||
case 'standard':
|
||||
return 'info';
|
||||
case PriorityLevel.INFO:
|
||||
case 'info':
|
||||
return 'check-circle';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
|
||||
export function getTypeClassBadgeVariant(
|
||||
typeClass: AlertTypeClass | string
|
||||
): 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'outline' {
|
||||
switch (typeClass) {
|
||||
case AlertTypeClass.ACTION_NEEDED:
|
||||
case 'action_needed':
|
||||
return 'error';
|
||||
case AlertTypeClass.PREVENTED_ISSUE:
|
||||
case 'prevented_issue':
|
||||
return 'success';
|
||||
case AlertTypeClass.TREND_WARNING:
|
||||
case 'trend_warning':
|
||||
return 'warning';
|
||||
case AlertTypeClass.ESCALATION:
|
||||
case 'escalation':
|
||||
return 'error';
|
||||
case AlertTypeClass.INFORMATION:
|
||||
case 'information':
|
||||
return 'info';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTimeUntilConsequence(hours?: number): string {
|
||||
if (!hours) return '';
|
||||
|
||||
if (hours < 1) {
|
||||
return `${Math.round(hours * 60)} minutes`;
|
||||
} else if (hours < 24) {
|
||||
return `${Math.round(hours)} hours`;
|
||||
} else {
|
||||
return `${Math.round(hours / 24)} days`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert legacy alert format to new Event format
|
||||
* This function provides backward compatibility for older alert structures
|
||||
*/
|
||||
export function convertLegacyAlert(legacyAlert: any): Event {
|
||||
// If it's already in the new format, return as-is
|
||||
if (legacyAlert.event_class && legacyAlert.event_class in EventClass) {
|
||||
return legacyAlert;
|
||||
}
|
||||
|
||||
// Convert legacy format to new format
|
||||
const newAlert: Event = {
|
||||
id: legacyAlert.id || legacyAlert.alert_id || '',
|
||||
tenant_id: legacyAlert.tenant_id || '',
|
||||
event_class: EventClass.ALERT, // Default to alert
|
||||
event_domain: legacyAlert.event_domain || '',
|
||||
event_type: legacyAlert.event_type || legacyAlert.type || '',
|
||||
service: legacyAlert.service || 'unknown',
|
||||
i18n: legacyAlert.i18n || {
|
||||
title_key: legacyAlert.title_key || legacyAlert.title || '',
|
||||
message_key: legacyAlert.message_key || legacyAlert.message || '',
|
||||
title_params: legacyAlert.title_params || {},
|
||||
message_params: legacyAlert.message_params || {},
|
||||
},
|
||||
priority_level: legacyAlert.priority_level || PriorityLevel.STANDARD,
|
||||
status: legacyAlert.status || 'active',
|
||||
created_at: legacyAlert.created_at || new Date().toISOString(),
|
||||
updated_at: legacyAlert.updated_at || new Date().toISOString(),
|
||||
event_metadata: legacyAlert.event_metadata || legacyAlert.metadata || {},
|
||||
};
|
||||
|
||||
return newAlert;
|
||||
}
|
||||
117
frontend/src/api/types/orchestrator.ts
Normal file
117
frontend/src/api/types/orchestrator.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Orchestrator API Types
|
||||
*/
|
||||
|
||||
export interface OrchestratorWorkflowRequest {
|
||||
target_date?: string; // YYYY-MM-DD, defaults to tomorrow
|
||||
planning_horizon_days?: number; // Default: 14
|
||||
|
||||
// Forecasting options
|
||||
forecast_days_ahead?: number; // Default: 7
|
||||
|
||||
// Production options
|
||||
auto_schedule_production?: boolean; // Default: true
|
||||
production_planning_days?: number; // Default: 1
|
||||
|
||||
// Procurement options
|
||||
auto_create_purchase_orders?: boolean; // Default: true
|
||||
auto_approve_purchase_orders?: boolean; // Default: false
|
||||
safety_stock_percentage?: number; // Default: 20.00
|
||||
|
||||
// Orchestrator options
|
||||
skip_on_error?: boolean; // Continue to next step if one fails
|
||||
notify_on_completion?: boolean; // Send notification when done
|
||||
}
|
||||
|
||||
export interface WorkflowStepResult {
|
||||
step: 'forecasting' | 'production' | 'procurement';
|
||||
status: 'success' | 'failed' | 'skipped';
|
||||
duration_ms: number;
|
||||
data?: any;
|
||||
error?: string;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface OrchestratorWorkflowResponse {
|
||||
success: boolean;
|
||||
workflow_id: string;
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
execution_date: string;
|
||||
total_duration_ms: number;
|
||||
|
||||
steps: WorkflowStepResult[];
|
||||
|
||||
// Step-specific results
|
||||
forecast_result?: {
|
||||
forecast_id: string;
|
||||
total_forecasts: number;
|
||||
forecast_data: any;
|
||||
};
|
||||
|
||||
production_result?: {
|
||||
schedule_id: string;
|
||||
total_batches: number;
|
||||
total_quantity: number;
|
||||
};
|
||||
|
||||
procurement_result?: {
|
||||
plan_id: string;
|
||||
total_requirements: number;
|
||||
total_cost: string;
|
||||
purchase_orders_created: number;
|
||||
purchase_orders_auto_approved: number;
|
||||
};
|
||||
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionSummary {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
started_at: string;
|
||||
completed_at?: string;
|
||||
total_duration_ms?: number;
|
||||
steps_completed: number;
|
||||
steps_total: number;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionDetail extends WorkflowExecutionSummary {
|
||||
steps: WorkflowStepResult[];
|
||||
forecast_id?: string;
|
||||
production_schedule_id?: string;
|
||||
procurement_plan_id?: string;
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface OrchestratorStatus {
|
||||
is_leader: boolean;
|
||||
scheduler_running: boolean;
|
||||
next_scheduled_run?: string;
|
||||
last_execution?: {
|
||||
id: string;
|
||||
target_date: string;
|
||||
status: string;
|
||||
completed_at: string;
|
||||
};
|
||||
total_executions_today: number;
|
||||
total_successful_executions: number;
|
||||
total_failed_executions: number;
|
||||
}
|
||||
|
||||
export interface OrchestratorConfig {
|
||||
enabled: boolean;
|
||||
schedule_cron: string; // Cron expression for daily run
|
||||
default_planning_horizon_days: number;
|
||||
auto_create_purchase_orders: boolean;
|
||||
auto_approve_purchase_orders: boolean;
|
||||
safety_stock_percentage: number;
|
||||
notify_on_completion: boolean;
|
||||
notify_on_failure: boolean;
|
||||
skip_on_error: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user