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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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 [];

View File

@@ -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
*/

View File

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

View File

@@ -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
// ===================================================================

View File

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

View File

@@ -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,

View File

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

View File

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

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

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