New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -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;
};

View File

@@ -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,
});
};

View File

@@ -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,
});
};

View File

@@ -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] });
},
});
}

View File

@@ -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,
});
};

View File

@@ -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
});
};

View 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,
});
}

View 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,
});
};

View 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,
});
};

View 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,
});
};

File diff suppressed because it is too large Load Diff

View 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 };
}