Improve the frontend 4
This commit is contained in:
@@ -18,22 +18,6 @@ import {
|
||||
GetCustomersParams,
|
||||
UpdateOrderStatusParams,
|
||||
GetDemandRequirementsParams,
|
||||
// Procurement types
|
||||
ProcurementPlanResponse,
|
||||
ProcurementPlanCreate,
|
||||
ProcurementPlanUpdate,
|
||||
ProcurementRequirementResponse,
|
||||
ProcurementRequirementUpdate,
|
||||
ProcurementDashboardData,
|
||||
GeneratePlanRequest,
|
||||
GeneratePlanResponse,
|
||||
PaginatedProcurementPlans,
|
||||
GetProcurementPlansParams,
|
||||
CreatePOsResult,
|
||||
LinkRequirementToPORequest,
|
||||
UpdateDeliveryStatusRequest,
|
||||
GetPlanRequirementsParams,
|
||||
UpdatePlanStatusParams,
|
||||
} from '../types/orders';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
|
||||
@@ -58,17 +42,6 @@ export const ordersKeys = {
|
||||
|
||||
// Status
|
||||
status: (tenantId: string) => [...ordersKeys.all, 'status', tenantId] as const,
|
||||
|
||||
// Procurement
|
||||
procurement: () => [...ordersKeys.all, 'procurement'] as const,
|
||||
procurementPlans: (params: GetProcurementPlansParams) => [...ordersKeys.procurement(), 'plans', params] as const,
|
||||
procurementPlan: (tenantId: string, planId: string) => [...ordersKeys.procurement(), 'plan', tenantId, planId] as const,
|
||||
procurementPlanByDate: (tenantId: string, date: string) => [...ordersKeys.procurement(), 'plan-by-date', tenantId, date] as const,
|
||||
currentProcurementPlan: (tenantId: string) => [...ordersKeys.procurement(), 'current-plan', tenantId] as const,
|
||||
procurementDashboard: (tenantId: string) => [...ordersKeys.procurement(), 'dashboard', tenantId] as const,
|
||||
planRequirements: (params: GetPlanRequirementsParams) => [...ordersKeys.procurement(), 'requirements', params] as const,
|
||||
criticalRequirements: (tenantId: string) => [...ordersKeys.procurement(), 'critical-requirements', tenantId] as const,
|
||||
procurementHealth: (tenantId: string) => [...ordersKeys.procurement(), 'health', tenantId] as const,
|
||||
} as const;
|
||||
|
||||
// ===== Order Queries =====
|
||||
@@ -360,378 +333,3 @@ export const useInvalidateOrders = () => {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ===== Procurement Queries =====
|
||||
|
||||
export const useProcurementPlans = (
|
||||
params: GetProcurementPlansParams,
|
||||
options?: Omit<UseQueryOptions<PaginatedProcurementPlans, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedProcurementPlans, ApiError>({
|
||||
queryKey: ordersKeys.procurementPlans(params),
|
||||
queryFn: () => OrdersService.getProcurementPlans(params),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
enabled: !!params.tenant_id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProcurementPlan = (
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||
queryKey: ordersKeys.procurementPlan(tenantId, planId),
|
||||
queryFn: () => OrdersService.getProcurementPlanById(tenantId, planId),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!tenantId && !!planId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProcurementPlanByDate = (
|
||||
tenantId: string,
|
||||
planDate: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||
queryKey: ordersKeys.procurementPlanByDate(tenantId, planDate),
|
||||
queryFn: () => OrdersService.getProcurementPlanByDate(tenantId, planDate),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!tenantId && !!planDate,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCurrentProcurementPlan = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||
queryKey: ordersKeys.currentProcurementPlan(tenantId),
|
||||
queryFn: () => OrdersService.getCurrentProcurementPlan(tenantId),
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProcurementDashboard = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementDashboardData | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementDashboardData | null, ApiError>({
|
||||
queryKey: ordersKeys.procurementDashboard(tenantId),
|
||||
queryFn: () => OrdersService.getProcurementDashboard(tenantId),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const usePlanRequirements = (
|
||||
params: GetPlanRequirementsParams,
|
||||
options?: Omit<UseQueryOptions<ProcurementRequirementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementRequirementResponse[], ApiError>({
|
||||
queryKey: ordersKeys.planRequirements(params),
|
||||
queryFn: () => OrdersService.getPlanRequirements(params),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!params.tenant_id && !!params.plan_id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCriticalRequirements = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementRequirementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementRequirementResponse[], ApiError>({
|
||||
queryKey: ordersKeys.criticalRequirements(tenantId),
|
||||
queryFn: () => OrdersService.getCriticalRequirements(tenantId),
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProcurementHealth = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }, ApiError>({
|
||||
queryKey: ordersKeys.procurementHealth(tenantId),
|
||||
queryFn: () => OrdersService.getProcurementHealth(tenantId),
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// ===== Procurement Mutations =====
|
||||
|
||||
export const useGenerateProcurementPlan = (
|
||||
options?: UseMutationOptions<GeneratePlanResponse, ApiError, { tenantId: string; request: GeneratePlanRequest }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GeneratePlanResponse, ApiError, { tenantId: string; request: GeneratePlanRequest }>({
|
||||
mutationFn: ({ tenantId, request }) => OrdersService.generateProcurementPlan(tenantId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all procurement queries for this tenant
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
|
||||
// If plan was generated successfully, cache it
|
||||
if (data.success && data.plan) {
|
||||
queryClient.setQueryData(
|
||||
ordersKeys.procurementPlan(variables.tenantId, data.plan.id),
|
||||
data.plan
|
||||
);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProcurementPlanStatus = (
|
||||
options?: UseMutationOptions<ProcurementPlanResponse, ApiError, UpdatePlanStatusParams>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ProcurementPlanResponse, ApiError, UpdatePlanStatusParams>({
|
||||
mutationFn: (params) => OrdersService.updateProcurementPlanStatus(params),
|
||||
onSuccess: (data, variables) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(
|
||||
ordersKeys.procurementPlan(variables.tenant_id, variables.plan_id),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate plans list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
const queryKey = query.queryKey as string[];
|
||||
return queryKey.includes('plans') &&
|
||||
JSON.stringify(queryKey).includes(variables.tenant_id);
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurementDashboard(variables.tenant_id),
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTriggerDailyScheduler = (
|
||||
options?: UseMutationOptions<{ success: boolean; message: string; tenant_id: string }, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean; message: string; tenant_id: string }, ApiError, string>({
|
||||
mutationFn: (tenantId) => OrdersService.triggerDailyScheduler(tenantId),
|
||||
onSuccess: (data, tenantId) => {
|
||||
// Invalidate all procurement data for this tenant
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// ===== NEW PROCUREMENT FEATURE HOOKS =====
|
||||
|
||||
/**
|
||||
* Hook to recalculate a procurement plan
|
||||
*/
|
||||
export const useRecalculateProcurementPlan = (
|
||||
options?: UseMutationOptions<GeneratePlanResponse, ApiError, { tenantId: string; planId: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GeneratePlanResponse, ApiError, { tenantId: string; planId: string }>({
|
||||
mutationFn: ({ tenantId, planId }) => OrdersService.recalculateProcurementPlan(tenantId, planId),
|
||||
onSuccess: (data, variables) => {
|
||||
if (data.plan) {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(
|
||||
ordersKeys.procurementPlan(variables.tenantId, variables.planId),
|
||||
data.plan
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate plans list and dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to approve a procurement plan
|
||||
*/
|
||||
export const useApproveProcurementPlan = (
|
||||
options?: UseMutationOptions<ProcurementPlanResponse, ApiError, { tenantId: string; planId: string; approval_notes?: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ProcurementPlanResponse, ApiError, { tenantId: string; planId: string; approval_notes?: string }>({
|
||||
mutationFn: ({ tenantId, planId, approval_notes }) =>
|
||||
OrdersService.approveProcurementPlan(tenantId, planId, { approval_notes }),
|
||||
onSuccess: (data, variables) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(
|
||||
ordersKeys.procurementPlan(variables.tenantId, variables.planId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate plans list and dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to reject a procurement plan
|
||||
*/
|
||||
export const useRejectProcurementPlan = (
|
||||
options?: UseMutationOptions<ProcurementPlanResponse, ApiError, { tenantId: string; planId: string; rejection_notes?: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ProcurementPlanResponse, ApiError, { tenantId: string; planId: string; rejection_notes?: string }>({
|
||||
mutationFn: ({ tenantId, planId, rejection_notes }) =>
|
||||
OrdersService.rejectProcurementPlan(tenantId, planId, { rejection_notes }),
|
||||
onSuccess: (data, variables) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(
|
||||
ordersKeys.procurementPlan(variables.tenantId, variables.planId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate plans list and dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to create purchase orders from procurement plan
|
||||
*/
|
||||
export const useCreatePurchaseOrdersFromPlan = (
|
||||
options?: UseMutationOptions<CreatePOsResult, ApiError, { tenantId: string; planId: string; autoApprove?: boolean }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreatePOsResult, ApiError, { tenantId: string; planId: string; autoApprove?: boolean }>({
|
||||
mutationFn: ({ tenantId, planId, autoApprove = false }) =>
|
||||
OrdersService.createPurchaseOrdersFromPlan(tenantId, planId, autoApprove),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate procurement plan to refresh requirements status
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurementPlan(variables.tenantId, variables.planId),
|
||||
});
|
||||
|
||||
// Invalidate dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurementDashboard(variables.tenantId),
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to link a requirement to a purchase order
|
||||
*/
|
||||
export const useLinkRequirementToPurchaseOrder = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string; requirement_id: string; purchase_order_id: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: LinkRequirementToPORequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ success: boolean; message: string; requirement_id: string; purchase_order_id: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: LinkRequirementToPORequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, requirementId, request }) =>
|
||||
OrdersService.linkRequirementToPurchaseOrder(tenantId, requirementId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate procurement data to refresh requirements
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update delivery status for a requirement
|
||||
*/
|
||||
export const useUpdateRequirementDeliveryStatus = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string; requirement_id: string; delivery_status: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ success: boolean; message: string; requirement_id: string; delivery_status: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, requirementId, request }) =>
|
||||
OrdersService.updateRequirementDeliveryStatus(tenantId, requirementId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate procurement data to refresh requirements
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.procurement(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
926
frontend/src/api/hooks/performance.ts
Normal file
926
frontend/src/api/hooks/performance.ts
Normal file
@@ -0,0 +1,926 @@
|
||||
/**
|
||||
* Performance Analytics Hooks
|
||||
* React Query hooks for fetching real-time performance data across all departments
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
PerformanceOverview,
|
||||
DepartmentPerformance,
|
||||
KPIMetric,
|
||||
PerformanceAlert,
|
||||
HourlyProductivity,
|
||||
ProductionPerformance,
|
||||
InventoryPerformance,
|
||||
SalesPerformance,
|
||||
ProcurementPerformance,
|
||||
TimePeriod,
|
||||
} from '../types/performance';
|
||||
import { useProductionDashboard } from './production';
|
||||
import { useInventoryDashboard } from './dashboard';
|
||||
import { useSalesAnalytics } from './sales';
|
||||
import { useProcurementDashboard } from './procurement';
|
||||
import { useOrdersDashboard } from './orders';
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
const getDateRangeForPeriod = (period: TimePeriod): { startDate: string; endDate: string } => {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
|
||||
switch (period) {
|
||||
case 'day':
|
||||
startDate.setDate(endDate.getDate() - 1);
|
||||
break;
|
||||
case 'week':
|
||||
startDate.setDate(endDate.getDate() - 7);
|
||||
break;
|
||||
case 'month':
|
||||
startDate.setMonth(endDate.getMonth() - 1);
|
||||
break;
|
||||
case 'quarter':
|
||||
startDate.setMonth(endDate.getMonth() - 3);
|
||||
break;
|
||||
case 'year':
|
||||
startDate.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';
|
||||
return change > 0 ? 'up' : 'down';
|
||||
};
|
||||
|
||||
const calculateStatus = (current: number, target: number): 'good' | 'warning' | 'critical' => {
|
||||
const percentage = (current / target) * 100;
|
||||
if (percentage >= 95) return 'good';
|
||||
if (percentage >= 85) return 'warning';
|
||||
return 'critical';
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Production Performance Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useProductionPerformance = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: dashboard, isLoading: dashboardLoading } = useProductionDashboard(tenantId);
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
const efficiencyPercentage = dashboard?.efficiency_percentage || 0;
|
||||
const qualityScore = dashboard?.average_quality_score || 0;
|
||||
const capacityUtilization = dashboard?.capacity_utilization || 0;
|
||||
const onTimeCompletionRate = dashboard?.on_time_completion_rate || 0;
|
||||
|
||||
const performance: ProductionPerformance | undefined = useMemo(() => {
|
||||
if (!dashboard) return undefined;
|
||||
|
||||
return {
|
||||
efficiency: efficiencyPercentage,
|
||||
average_batch_time: 0, // Not available in dashboard
|
||||
quality_rate: qualityScore,
|
||||
waste_percentage: 0, // Would need production-trends endpoint
|
||||
capacity_utilization: capacityUtilization,
|
||||
equipment_efficiency: capacityUtilization,
|
||||
on_time_completion_rate: onTimeCompletionRate,
|
||||
yield_rate: 0, // Would need production-trends endpoint
|
||||
};
|
||||
}, [efficiencyPercentage, qualityScore, capacityUtilization, onTimeCompletionRate]);
|
||||
|
||||
return {
|
||||
data: performance,
|
||||
isLoading: dashboardLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Inventory Performance Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useInventoryPerformance = (tenantId: string) => {
|
||||
const { data: dashboard, isLoading: dashboardLoading } = useInventoryDashboard(tenantId);
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
const totalItems = dashboard?.total_ingredients || 1;
|
||||
const lowStockCount = dashboard?.low_stock_items || 0;
|
||||
const outOfStockCount = dashboard?.out_of_stock_items || 0;
|
||||
const foodSafetyAlertsActive = dashboard?.food_safety_alerts_active || 0;
|
||||
const expiringItems = dashboard?.expiring_soon_items || 0;
|
||||
const stockValue = dashboard?.total_stock_value || 0;
|
||||
|
||||
const performance: InventoryPerformance | undefined = useMemo(() => {
|
||||
if (!dashboard) return undefined;
|
||||
|
||||
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
|
||||
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]);
|
||||
|
||||
return {
|
||||
data: performance,
|
||||
isLoading: dashboardLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Sales Performance Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useSalesPerformance = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { startDate, endDate } = getDateRangeForPeriod(period);
|
||||
|
||||
const { data: salesData, isLoading: salesLoading } = useSalesAnalytics(
|
||||
tenantId,
|
||||
startDate,
|
||||
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 performance: SalesPerformance | undefined = useMemo(() => {
|
||||
if (!salesData) return undefined;
|
||||
|
||||
const topProducts = JSON.parse(topProductsString);
|
||||
|
||||
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
|
||||
top_products: Array.isArray(topProducts)
|
||||
? topProducts.map((product: any) => ({
|
||||
product_id: product.inventory_product_id || '',
|
||||
product_name: product.product_name || '',
|
||||
sales: product.total_quantity || 0,
|
||||
revenue: product.total_revenue || 0,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}, [totalRevenue, totalTransactions, avgTransactionValue, topProductsString]);
|
||||
|
||||
return {
|
||||
data: performance,
|
||||
isLoading: salesLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Procurement Performance Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useProcurementPerformance = (tenantId: string) => {
|
||||
const { data: dashboard, isLoading } = useProcurementDashboard(tenantId);
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
const avgFulfillmentRate = dashboard?.performance_metrics?.average_fulfillment_rate || 0;
|
||||
const avgOnTimeDelivery = dashboard?.performance_metrics?.average_on_time_delivery || 0;
|
||||
const costAccuracy = dashboard?.performance_metrics?.cost_accuracy || 0;
|
||||
const supplierPerformance = dashboard?.performance_metrics?.supplier_performance || 0;
|
||||
const totalPlans = dashboard?.summary?.total_plans || 0;
|
||||
const lowStockCount = dashboard?.low_stock_alerts?.length || 0;
|
||||
const overdueCount = dashboard?.overdue_requirements?.length || 0;
|
||||
|
||||
const performance: ProcurementPerformance | undefined = useMemo(() => {
|
||||
if (!dashboard) return undefined;
|
||||
|
||||
return {
|
||||
fulfillment_rate: avgFulfillmentRate,
|
||||
on_time_delivery_rate: avgOnTimeDelivery,
|
||||
cost_accuracy: costAccuracy,
|
||||
supplier_performance_score: supplierPerformance,
|
||||
active_plans: totalPlans,
|
||||
critical_requirements: lowStockCount + overdueCount,
|
||||
};
|
||||
}, [avgFulfillmentRate, avgOnTimeDelivery, costAccuracy, supplierPerformance, totalPlans, lowStockCount, overdueCount]);
|
||||
|
||||
return {
|
||||
data: performance,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Performance Overview Hook (Aggregates All Departments)
|
||||
// ============================================================================
|
||||
|
||||
export const usePerformanceOverview = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period);
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId);
|
||||
const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId);
|
||||
|
||||
const overview: PerformanceOverview | undefined = useMemo(() => {
|
||||
if (!production || !inventory || !sales || !procurement || !orders) return undefined;
|
||||
|
||||
// Calculate customer satisfaction from order fulfillment and delivery performance
|
||||
const totalOrders = orders.total_orders_today || 1;
|
||||
const deliveredOrders = orders.delivered_orders || 0;
|
||||
const orderFulfillmentRate = (deliveredOrders / totalOrders) * 100;
|
||||
|
||||
const customerSatisfaction = (orderFulfillmentRate + procurement.on_time_delivery_rate) / 2;
|
||||
|
||||
return {
|
||||
overall_efficiency: production.efficiency,
|
||||
average_production_time: production.average_batch_time,
|
||||
quality_score: production.quality_rate,
|
||||
employee_productivity: production.capacity_utilization,
|
||||
customer_satisfaction: customerSatisfaction,
|
||||
resource_utilization: production.equipment_efficiency || production.capacity_utilization,
|
||||
};
|
||||
}, [production, inventory, sales, procurement, orders]);
|
||||
|
||||
return {
|
||||
data: overview,
|
||||
isLoading: productionLoading || inventoryLoading || salesLoading || procurementLoading || ordersLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Department Performance Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useDepartmentPerformance = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period);
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const productionEfficiency = production?.efficiency || 0;
|
||||
const productionAvgBatchTime = production?.average_batch_time || 0;
|
||||
const productionQualityRate = production?.quality_rate || 0;
|
||||
const productionWastePercentage = production?.waste_percentage || 0;
|
||||
const salesTotalRevenue = sales?.total_revenue || 0;
|
||||
const salesGrowthRate = sales?.growth_rate || 0;
|
||||
const salesTotalTransactions = sales?.total_transactions || 0;
|
||||
const salesAvgTransactionValue = sales?.average_transaction_value || 0;
|
||||
const inventoryStockAccuracy = inventory?.stock_accuracy || 0;
|
||||
const inventoryLowStockCount = inventory?.low_stock_count || 0;
|
||||
const inventoryTurnoverRate = inventory?.turnover_rate || 0;
|
||||
const procurementFulfillmentRate = procurement?.fulfillment_rate || 0;
|
||||
const procurementOnTimeDeliveryRate = procurement?.on_time_delivery_rate || 0;
|
||||
const procurementCostAccuracy = procurement?.cost_accuracy || 0;
|
||||
|
||||
const departments: DepartmentPerformance[] | undefined = useMemo(() => {
|
||||
if (!production || !inventory || !sales || !procurement) return undefined;
|
||||
|
||||
return [
|
||||
{
|
||||
department_id: 'production',
|
||||
department_name: 'Producción',
|
||||
efficiency: productionEfficiency,
|
||||
trend: productionEfficiency >= 85 ? 'up' : productionEfficiency >= 75 ? 'stable' : 'down',
|
||||
metrics: {
|
||||
primary_metric: {
|
||||
label: 'Tiempo promedio de lote',
|
||||
value: productionAvgBatchTime,
|
||||
unit: 'h',
|
||||
},
|
||||
secondary_metric: {
|
||||
label: 'Tasa de calidad',
|
||||
value: productionQualityRate,
|
||||
unit: '%',
|
||||
},
|
||||
tertiary_metric: {
|
||||
label: 'Desperdicio',
|
||||
value: productionWastePercentage,
|
||||
unit: '%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
department_id: 'sales',
|
||||
department_name: 'Ventas',
|
||||
efficiency: (salesTotalRevenue / 10000) * 100, // Normalize to percentage
|
||||
trend: salesGrowthRate > 0 ? 'up' : salesGrowthRate < 0 ? 'down' : 'stable',
|
||||
metrics: {
|
||||
primary_metric: {
|
||||
label: 'Ingresos totales',
|
||||
value: salesTotalRevenue,
|
||||
unit: '€',
|
||||
},
|
||||
secondary_metric: {
|
||||
label: 'Transacciones',
|
||||
value: salesTotalTransactions,
|
||||
unit: '',
|
||||
},
|
||||
tertiary_metric: {
|
||||
label: 'Valor promedio',
|
||||
value: salesAvgTransactionValue,
|
||||
unit: '€',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
department_id: 'inventory',
|
||||
department_name: 'Inventario',
|
||||
efficiency: inventoryStockAccuracy,
|
||||
trend: inventoryLowStockCount < 5 ? 'up' : inventoryLowStockCount < 10 ? 'stable' : 'down',
|
||||
metrics: {
|
||||
primary_metric: {
|
||||
label: 'Rotación de stock',
|
||||
value: inventoryTurnoverRate,
|
||||
unit: 'x',
|
||||
},
|
||||
secondary_metric: {
|
||||
label: 'Precisión',
|
||||
value: inventoryStockAccuracy,
|
||||
unit: '%',
|
||||
},
|
||||
tertiary_metric: {
|
||||
label: 'Items con bajo stock',
|
||||
value: inventoryLowStockCount,
|
||||
unit: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
department_id: 'administration',
|
||||
department_name: 'Administración',
|
||||
efficiency: procurementFulfillmentRate,
|
||||
trend: procurementOnTimeDeliveryRate >= 95 ? 'up' : procurementOnTimeDeliveryRate >= 85 ? 'stable' : 'down',
|
||||
metrics: {
|
||||
primary_metric: {
|
||||
label: 'Tasa de cumplimiento',
|
||||
value: procurementFulfillmentRate,
|
||||
unit: '%',
|
||||
},
|
||||
secondary_metric: {
|
||||
label: 'Entrega a tiempo',
|
||||
value: procurementOnTimeDeliveryRate,
|
||||
unit: '%',
|
||||
},
|
||||
tertiary_metric: {
|
||||
label: 'Precisión de costos',
|
||||
value: procurementCostAccuracy,
|
||||
unit: '%',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [
|
||||
productionEfficiency,
|
||||
productionAvgBatchTime,
|
||||
productionQualityRate,
|
||||
productionWastePercentage,
|
||||
salesTotalRevenue,
|
||||
salesGrowthRate,
|
||||
salesTotalTransactions,
|
||||
salesAvgTransactionValue,
|
||||
inventoryStockAccuracy,
|
||||
inventoryLowStockCount,
|
||||
inventoryTurnoverRate,
|
||||
procurementFulfillmentRate,
|
||||
procurementOnTimeDeliveryRate,
|
||||
procurementCostAccuracy,
|
||||
]);
|
||||
|
||||
return {
|
||||
data: departments,
|
||||
isLoading: productionLoading || inventoryLoading || salesLoading || procurementLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// KPI Metrics Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: procurement, isLoading: procurementLoading } = 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;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'overall-efficiency',
|
||||
name: 'Eficiencia General',
|
||||
current_value: production.efficiency,
|
||||
target_value: 90,
|
||||
previous_value: previousProduction,
|
||||
unit: '%',
|
||||
trend: calculateTrend(production.efficiency, previousProduction),
|
||||
status: calculateStatus(production.efficiency, 90),
|
||||
},
|
||||
{
|
||||
id: 'quality-rate',
|
||||
name: 'Tasa de Calidad',
|
||||
current_value: production.quality_rate,
|
||||
target_value: 95,
|
||||
previous_value: previousQuality,
|
||||
unit: '%',
|
||||
trend: calculateTrend(production.quality_rate, previousQuality),
|
||||
status: calculateStatus(production.quality_rate, 95),
|
||||
},
|
||||
{
|
||||
id: 'on-time-delivery',
|
||||
name: 'Entrega a Tiempo',
|
||||
current_value: procurement.on_time_delivery_rate,
|
||||
target_value: 95,
|
||||
previous_value: previousProcurement,
|
||||
unit: '%',
|
||||
trend: calculateTrend(procurement.on_time_delivery_rate, previousProcurement),
|
||||
status: calculateStatus(procurement.on_time_delivery_rate, 95),
|
||||
},
|
||||
{
|
||||
id: 'inventory-accuracy',
|
||||
name: 'Precisión de Inventario',
|
||||
current_value: inventory.stock_accuracy,
|
||||
target_value: 98,
|
||||
previous_value: previousInventory,
|
||||
unit: '%',
|
||||
trend: calculateTrend(inventory.stock_accuracy, previousInventory),
|
||||
status: calculateStatus(inventory.stock_accuracy, 98),
|
||||
},
|
||||
];
|
||||
}, [production, inventory, procurement]);
|
||||
|
||||
return {
|
||||
data: kpis,
|
||||
isLoading: productionLoading || inventoryLoading || procurementLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Performance Alerts Hook
|
||||
// ============================================================================
|
||||
|
||||
export const usePerformanceAlerts = (tenantId: string) => {
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryDashboard(tenantId);
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementDashboard(tenantId);
|
||||
|
||||
// Extract primitive values to prevent unnecessary recalculations
|
||||
const lowStockCount = inventory?.low_stock_items || 0;
|
||||
const outOfStockCount = inventory?.out_of_stock_items || 0;
|
||||
const foodSafetyAlerts = inventory?.food_safety_alerts_active || 0;
|
||||
const expiringCount = inventory?.expiring_soon_items || 0;
|
||||
const lowStockAlertsCount = procurement?.low_stock_alerts?.length || 0;
|
||||
const overdueReqsCount = procurement?.overdue_requirements?.length || 0;
|
||||
|
||||
const alerts: PerformanceAlert[] | undefined = useMemo(() => {
|
||||
if (!inventory || !procurement) return undefined;
|
||||
|
||||
const alertsList: PerformanceAlert[] = [];
|
||||
|
||||
// Low stock alerts
|
||||
if (lowStockCount > 0) {
|
||||
alertsList.push({
|
||||
id: `low-stock-${Date.now()}`,
|
||||
type: 'warning',
|
||||
department: 'Inventario',
|
||||
message: `${lowStockCount} ingredientes con stock bajo`,
|
||||
timestamp: new Date().toISOString(),
|
||||
metric_affected: 'Stock',
|
||||
current_value: lowStockCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Out of stock alerts
|
||||
if (outOfStockCount > 0) {
|
||||
alertsList.push({
|
||||
id: `out-of-stock-${Date.now()}`,
|
||||
type: 'critical',
|
||||
department: 'Inventario',
|
||||
message: `${outOfStockCount} ingredientes sin stock`,
|
||||
timestamp: new Date().toISOString(),
|
||||
metric_affected: 'Stock',
|
||||
current_value: outOfStockCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Food safety alerts
|
||||
if (foodSafetyAlerts > 0) {
|
||||
alertsList.push({
|
||||
id: `food-safety-${Date.now()}`,
|
||||
type: 'critical',
|
||||
department: 'Inventario',
|
||||
message: `${foodSafetyAlerts} alertas de seguridad alimentaria activas`,
|
||||
timestamp: new Date().toISOString(),
|
||||
metric_affected: 'Seguridad Alimentaria',
|
||||
current_value: foodSafetyAlerts,
|
||||
});
|
||||
}
|
||||
|
||||
// Expiring items alerts
|
||||
if (expiringCount > 0) {
|
||||
alertsList.push({
|
||||
id: `expiring-${Date.now()}`,
|
||||
type: 'warning',
|
||||
department: 'Inventario',
|
||||
message: `${expiringCount} ingredientes próximos a vencer`,
|
||||
timestamp: new Date().toISOString(),
|
||||
metric_affected: 'Caducidad',
|
||||
current_value: expiringCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Critical procurement requirements
|
||||
const criticalCount = lowStockAlertsCount + overdueReqsCount;
|
||||
|
||||
if (criticalCount > 0) {
|
||||
alertsList.push({
|
||||
id: `procurement-critical-${Date.now()}`,
|
||||
type: 'warning',
|
||||
department: 'Administración',
|
||||
message: `${criticalCount} requisitos de compra críticos`,
|
||||
timestamp: new Date().toISOString(),
|
||||
metric_affected: 'Aprovisionamiento',
|
||||
current_value: criticalCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by severity: critical > warning > info
|
||||
return alertsList.sort((a, b) => {
|
||||
const severityOrder = { critical: 0, warning: 1, info: 2 };
|
||||
return severityOrder[a.type] - severityOrder[b.type];
|
||||
});
|
||||
}, [lowStockCount, outOfStockCount, foodSafetyAlerts, expiringCount, lowStockAlertsCount, overdueReqsCount]);
|
||||
|
||||
return {
|
||||
data: alerts || [],
|
||||
isLoading: inventoryLoading || procurementLoading,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hourly Productivity Hook
|
||||
// ============================================================================
|
||||
|
||||
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
|
||||
|
||||
return useQuery<HourlyProductivity[]>({
|
||||
queryKey: ['performance', 'hourly', tenantId],
|
||||
queryFn: async () => {
|
||||
// Placeholder - backend endpoint needed for real hourly data
|
||||
return [];
|
||||
},
|
||||
enabled: false, // Disable until backend endpoint is ready
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Cross-Functional Performance Metrics
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Cycle Time: Order-to-Delivery
|
||||
* Measures the complete time from order creation to delivery
|
||||
*/
|
||||
export const useCycleTimeMetrics = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId);
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const totalOrders = orders?.total_orders_today || 1;
|
||||
const deliveredOrders = orders?.delivered_orders || 0;
|
||||
const pendingOrders = orders?.pending_orders || 0;
|
||||
const avgProductionTime = production?.average_batch_time || 2;
|
||||
const onTimeCompletionRate = production?.on_time_completion_rate || 0;
|
||||
|
||||
const cycleTime = useMemo(() => {
|
||||
if (!orders || !production) return undefined;
|
||||
|
||||
// Estimate average cycle time based on fulfillment rate and production efficiency
|
||||
const fulfillmentRate = (deliveredOrders / totalOrders) * 100;
|
||||
|
||||
// Estimated total cycle time includes: order processing + production + delivery
|
||||
const estimatedCycleTime = avgProductionTime + (pendingOrders > 0 ? 1.5 : 0.5); // Add wait time
|
||||
|
||||
return {
|
||||
average_cycle_time: estimatedCycleTime,
|
||||
order_to_production_time: 0.5, // Order processing time
|
||||
production_time: avgProductionTime,
|
||||
production_to_delivery_time: pendingOrders > 0 ? 1.0 : 0.3,
|
||||
fulfillment_rate: fulfillmentRate,
|
||||
on_time_delivery_rate: onTimeCompletionRate,
|
||||
};
|
||||
}, [totalOrders, deliveredOrders, pendingOrders, avgProductionTime, onTimeCompletionRate]);
|
||||
|
||||
return {
|
||||
data: cycleTime,
|
||||
isLoading: ordersLoading || productionLoading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Process Efficiency Score
|
||||
* Combined efficiency across all departments
|
||||
*/
|
||||
export const useProcessEfficiencyScore = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId);
|
||||
const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const productionEfficiency = production?.efficiency || 0;
|
||||
const inventoryStockAccuracy = inventory?.stock_accuracy || 0;
|
||||
const procurementFulfillmentRate = procurement?.fulfillment_rate || 0;
|
||||
const totalOrders = orders?.total_orders_today || 1;
|
||||
const deliveredOrders = orders?.delivered_orders || 0;
|
||||
|
||||
const score = useMemo(() => {
|
||||
if (!production || !inventory || !procurement || !orders) return undefined;
|
||||
|
||||
// Weighted efficiency score across departments
|
||||
const productionWeight = 0.35;
|
||||
const inventoryWeight = 0.25;
|
||||
const procurementWeight = 0.25;
|
||||
const ordersWeight = 0.15;
|
||||
|
||||
const orderEfficiency = (deliveredOrders / totalOrders) * 100;
|
||||
|
||||
const overallScore =
|
||||
(productionEfficiency * productionWeight) +
|
||||
(inventoryStockAccuracy * inventoryWeight) +
|
||||
(procurementFulfillmentRate * procurementWeight) +
|
||||
(orderEfficiency * ordersWeight);
|
||||
|
||||
return {
|
||||
overall_score: overallScore,
|
||||
production_efficiency: productionEfficiency,
|
||||
inventory_efficiency: inventoryStockAccuracy,
|
||||
procurement_efficiency: procurementFulfillmentRate,
|
||||
order_efficiency: orderEfficiency,
|
||||
breakdown: {
|
||||
production: { value: productionEfficiency, weight: productionWeight * 100 },
|
||||
inventory: { value: inventoryStockAccuracy, weight: inventoryWeight * 100 },
|
||||
procurement: { value: procurementFulfillmentRate, weight: procurementWeight * 100 },
|
||||
orders: { value: orderEfficiency, weight: ordersWeight * 100 },
|
||||
},
|
||||
};
|
||||
}, [productionEfficiency, inventoryStockAccuracy, procurementFulfillmentRate, totalOrders, deliveredOrders]);
|
||||
|
||||
return {
|
||||
data: score,
|
||||
isLoading: productionLoading || inventoryLoading || procurementLoading || ordersLoading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource Utilization Rate
|
||||
* Cross-departmental resource balance and utilization
|
||||
*/
|
||||
export const useResourceUtilization = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const equipmentEfficiency = production?.equipment_efficiency || 0;
|
||||
const turnoverRate = inventory?.turnover_rate || 0;
|
||||
const stockAccuracy = inventory?.stock_accuracy || 0;
|
||||
const capacityUtilization = production?.capacity_utilization || 0;
|
||||
|
||||
const utilization = useMemo(() => {
|
||||
if (!production || !inventory) return undefined;
|
||||
|
||||
// Equipment utilization from production
|
||||
const equipmentUtilization = equipmentEfficiency;
|
||||
|
||||
// Inventory utilization based on turnover and stock levels
|
||||
const inventoryUtilization = turnoverRate > 0
|
||||
? Math.min(turnoverRate * 10, 100) // Normalize turnover to percentage
|
||||
: stockAccuracy;
|
||||
|
||||
// Combined resource utilization
|
||||
const overallUtilization = (equipmentUtilization + inventoryUtilization) / 2;
|
||||
|
||||
return {
|
||||
overall_utilization: overallUtilization,
|
||||
equipment_utilization: equipmentUtilization,
|
||||
inventory_utilization: inventoryUtilization,
|
||||
capacity_used: capacityUtilization,
|
||||
resource_balance: Math.abs(equipmentUtilization - inventoryUtilization) < 10 ? 'balanced' : 'imbalanced',
|
||||
};
|
||||
}, [equipmentEfficiency, turnoverRate, stockAccuracy, capacityUtilization]);
|
||||
|
||||
return {
|
||||
data: utilization,
|
||||
isLoading: productionLoading || inventoryLoading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cost-to-Revenue Ratio
|
||||
* Overall profitability metric
|
||||
*/
|
||||
export const useCostRevenueRatio = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const totalRevenue = sales?.total_revenue || 0;
|
||||
const stockValue = inventory?.stock_value || 0;
|
||||
const wastePercentage = production?.waste_percentage || 0;
|
||||
|
||||
const ratio = useMemo(() => {
|
||||
if (!sales || !inventory || !production) return undefined;
|
||||
|
||||
// Estimate costs from inventory value and waste
|
||||
const inventoryCosts = stockValue * 0.1; // Approximate monthly inventory cost
|
||||
const wasteCosts = (stockValue * wastePercentage) / 100;
|
||||
const estimatedTotalCosts = inventoryCosts + wasteCosts;
|
||||
|
||||
const costRevenueRatio = totalRevenue > 0 ? (estimatedTotalCosts / totalRevenue) * 100 : 0;
|
||||
const profitMargin = totalRevenue > 0 ? ((totalRevenue - estimatedTotalCosts) / totalRevenue) * 100 : 0;
|
||||
|
||||
return {
|
||||
cost_revenue_ratio: costRevenueRatio,
|
||||
profit_margin: profitMargin,
|
||||
total_revenue: totalRevenue,
|
||||
estimated_costs: estimatedTotalCosts,
|
||||
inventory_costs: inventoryCosts,
|
||||
waste_costs: wasteCosts,
|
||||
};
|
||||
}, [totalRevenue, stockValue, wastePercentage]);
|
||||
|
||||
return {
|
||||
data: ratio,
|
||||
isLoading: salesLoading || inventoryLoading || productionLoading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Quality Impact Index
|
||||
* Quality issues across all departments
|
||||
*/
|
||||
export const useQualityImpactIndex = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const productionQuality = production?.quality_rate || 0;
|
||||
const wasteImpact = production?.waste_percentage || 0;
|
||||
const expiringItems = inventory?.expiring_items_count || 0;
|
||||
const lowStockItems = inventory?.low_stock_count || 0;
|
||||
|
||||
const qualityIndex = useMemo(() => {
|
||||
if (!production || !inventory) return undefined;
|
||||
|
||||
// Inventory quality
|
||||
const totalItems = (expiringItems + lowStockItems) || 1;
|
||||
const inventoryQualityScore = 100 - ((expiringItems + lowStockItems) / totalItems * 10);
|
||||
|
||||
// Combined quality index (weighted average)
|
||||
const overallQuality = (productionQuality * 0.7) + (inventoryQualityScore * 0.3);
|
||||
|
||||
return {
|
||||
overall_quality_index: overallQuality,
|
||||
production_quality: productionQuality,
|
||||
inventory_quality: inventoryQualityScore,
|
||||
waste_impact: wasteImpact,
|
||||
quality_issues: {
|
||||
production_defects: 100 - productionQuality,
|
||||
waste_percentage: wasteImpact,
|
||||
expiring_items: expiringItems,
|
||||
low_stock_affecting_quality: lowStockItems,
|
||||
},
|
||||
};
|
||||
}, [productionQuality, wasteImpact, expiringItems, lowStockItems]);
|
||||
|
||||
return {
|
||||
data: qualityIndex,
|
||||
isLoading: productionLoading || inventoryLoading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Critical Bottlenecks
|
||||
* Identifies process bottlenecks across operations
|
||||
*/
|
||||
export const useCriticalBottlenecks = (tenantId: string, period: TimePeriod = 'week') => {
|
||||
const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period);
|
||||
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
|
||||
const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId);
|
||||
const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId);
|
||||
|
||||
// Extract primitive values before useMemo to prevent unnecessary recalculations
|
||||
const capacityUtilization = production?.capacity_utilization || 0;
|
||||
const onTimeCompletionRate = production?.on_time_completion_rate || 0;
|
||||
const lowStockCount = inventory?.low_stock_count || 0;
|
||||
const criticalRequirements = procurement?.critical_requirements || 0;
|
||||
const onTimeDeliveryRate = procurement?.on_time_delivery_rate || 0;
|
||||
const totalOrders = orders?.total_orders_today || 1;
|
||||
const pendingOrders = orders?.pending_orders || 0;
|
||||
|
||||
const bottlenecks = useMemo(() => {
|
||||
if (!production || !inventory || !procurement || !orders) return undefined;
|
||||
|
||||
const bottlenecksList = [];
|
||||
|
||||
// Production bottlenecks
|
||||
if (capacityUtilization > 90) {
|
||||
bottlenecksList.push({
|
||||
area: 'production',
|
||||
severity: 'high',
|
||||
description: 'Capacidad de producción al límite',
|
||||
metric: 'capacity_utilization',
|
||||
value: capacityUtilization,
|
||||
});
|
||||
}
|
||||
|
||||
if (onTimeCompletionRate < 85) {
|
||||
bottlenecksList.push({
|
||||
area: 'production',
|
||||
severity: 'medium',
|
||||
description: 'Retrasos en completitud de producción',
|
||||
metric: 'on_time_completion',
|
||||
value: onTimeCompletionRate,
|
||||
});
|
||||
}
|
||||
|
||||
// Inventory bottlenecks
|
||||
if (lowStockCount > 10) {
|
||||
bottlenecksList.push({
|
||||
area: 'inventory',
|
||||
severity: 'high',
|
||||
description: 'Alto número de ingredientes con stock bajo',
|
||||
metric: 'low_stock_count',
|
||||
value: lowStockCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Procurement bottlenecks
|
||||
if (criticalRequirements > 5) {
|
||||
bottlenecksList.push({
|
||||
area: 'procurement',
|
||||
severity: 'high',
|
||||
description: 'Requisitos de compra críticos pendientes',
|
||||
metric: 'critical_requirements',
|
||||
value: criticalRequirements,
|
||||
});
|
||||
}
|
||||
|
||||
if (onTimeDeliveryRate < 85) {
|
||||
bottlenecksList.push({
|
||||
area: 'procurement',
|
||||
severity: 'medium',
|
||||
description: 'Entregas de proveedores retrasadas',
|
||||
metric: 'on_time_delivery',
|
||||
value: onTimeDeliveryRate,
|
||||
});
|
||||
}
|
||||
|
||||
// Orders bottlenecks
|
||||
const pendingRate = (pendingOrders / totalOrders) * 100;
|
||||
|
||||
if (pendingRate > 30) {
|
||||
bottlenecksList.push({
|
||||
area: 'orders',
|
||||
severity: 'medium',
|
||||
description: 'Alto volumen de pedidos pendientes',
|
||||
metric: 'pending_orders',
|
||||
value: pendingOrders,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
total_bottlenecks: bottlenecksList.length,
|
||||
critical_count: bottlenecksList.filter(b => b.severity === 'high').length,
|
||||
bottlenecks: bottlenecksList,
|
||||
most_critical_area: bottlenecksList.length > 0
|
||||
? bottlenecksList.sort((a, b) => {
|
||||
const severityOrder = { high: 0, medium: 1, low: 2 };
|
||||
return severityOrder[a.severity as 'high' | 'medium' | 'low'] - severityOrder[b.severity as 'high' | 'medium' | 'low'];
|
||||
})[0].area
|
||||
: null,
|
||||
};
|
||||
}, [capacityUtilization, onTimeCompletionRate, lowStockCount, criticalRequirements, onTimeDeliveryRate, totalOrders, pendingOrders]);
|
||||
|
||||
return {
|
||||
data: bottlenecks,
|
||||
isLoading: productionLoading || inventoryLoading || procurementLoading || ordersLoading,
|
||||
};
|
||||
};
|
||||
495
frontend/src/api/hooks/procurement.ts
Normal file
495
frontend/src/api/hooks/procurement.ts
Normal file
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* Procurement React Query hooks
|
||||
* All hooks use the ProcurementService which connects to the standalone Procurement Service backend
|
||||
*/
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryOptions,
|
||||
UseMutationOptions,
|
||||
} from '@tanstack/react-query';
|
||||
import { ProcurementService } from '../services/procurement-service';
|
||||
import {
|
||||
// Response types
|
||||
ProcurementPlanResponse,
|
||||
ProcurementRequirementResponse,
|
||||
ProcurementDashboardData,
|
||||
ProcurementTrendsData,
|
||||
PaginatedProcurementPlans,
|
||||
GeneratePlanResponse,
|
||||
CreatePOsResult,
|
||||
|
||||
// Request types
|
||||
GeneratePlanRequest,
|
||||
AutoGenerateProcurementRequest,
|
||||
AutoGenerateProcurementResponse,
|
||||
LinkRequirementToPORequest,
|
||||
UpdateDeliveryStatusRequest,
|
||||
|
||||
// Query param types
|
||||
GetProcurementPlansParams,
|
||||
GetPlanRequirementsParams,
|
||||
UpdatePlanStatusParams,
|
||||
} from '../types/procurement';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
|
||||
// ===================================================================
|
||||
// QUERY KEYS
|
||||
// ===================================================================
|
||||
|
||||
export const procurementKeys = {
|
||||
all: ['procurement'] as const,
|
||||
|
||||
// Analytics & Dashboard
|
||||
analytics: (tenantId: string) => [...procurementKeys.all, 'analytics', tenantId] as const,
|
||||
trends: (tenantId: string, days: number) => [...procurementKeys.all, 'trends', tenantId, days] as const,
|
||||
|
||||
// Plans
|
||||
plans: () => [...procurementKeys.all, 'plans'] as const,
|
||||
plansList: (params: GetProcurementPlansParams) => [...procurementKeys.plans(), 'list', params] as const,
|
||||
plan: (tenantId: string, planId: string) => [...procurementKeys.plans(), 'detail', tenantId, planId] as const,
|
||||
planByDate: (tenantId: string, date: string) => [...procurementKeys.plans(), 'by-date', tenantId, date] as const,
|
||||
currentPlan: (tenantId: string) => [...procurementKeys.plans(), 'current', tenantId] as const,
|
||||
|
||||
// Requirements
|
||||
requirements: () => [...procurementKeys.all, 'requirements'] as const,
|
||||
planRequirements: (params: GetPlanRequirementsParams) =>
|
||||
[...procurementKeys.requirements(), 'plan', params] as const,
|
||||
criticalRequirements: (tenantId: string) =>
|
||||
[...procurementKeys.requirements(), 'critical', tenantId] as const,
|
||||
} as const;
|
||||
|
||||
// ===================================================================
|
||||
// ANALYTICS & DASHBOARD QUERIES
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get procurement analytics dashboard data
|
||||
*/
|
||||
export const useProcurementDashboard = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementDashboardData, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementDashboardData, ApiError>({
|
||||
queryKey: procurementKeys.analytics(tenantId),
|
||||
queryFn: () => ProcurementService.getProcurementAnalytics(tenantId),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get procurement time-series trends for charts
|
||||
*/
|
||||
export const useProcurementTrends = (
|
||||
tenantId: string,
|
||||
days: number = 7,
|
||||
options?: Omit<UseQueryOptions<ProcurementTrendsData, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementTrendsData, ApiError>({
|
||||
queryKey: procurementKeys.trends(tenantId, days),
|
||||
queryFn: () => ProcurementService.getProcurementTrends(tenantId, days),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// PROCUREMENT PLAN QUERIES
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get list of procurement plans with pagination and filtering
|
||||
*/
|
||||
export const useProcurementPlans = (
|
||||
params: GetProcurementPlansParams,
|
||||
options?: Omit<UseQueryOptions<PaginatedProcurementPlans, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PaginatedProcurementPlans, ApiError>({
|
||||
queryKey: procurementKeys.plansList(params),
|
||||
queryFn: () => ProcurementService.getProcurementPlans(params),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
enabled: !!params.tenant_id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single procurement plan by ID
|
||||
*/
|
||||
export const useProcurementPlan = (
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||
queryKey: procurementKeys.plan(tenantId, planId),
|
||||
queryFn: () => ProcurementService.getProcurementPlanById(tenantId, planId),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!tenantId && !!planId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get procurement plan for a specific date
|
||||
*/
|
||||
export const useProcurementPlanByDate = (
|
||||
tenantId: string,
|
||||
planDate: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||
queryKey: procurementKeys.planByDate(tenantId, planDate),
|
||||
queryFn: () => ProcurementService.getProcurementPlanByDate(tenantId, planDate),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!tenantId && !!planDate,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current day's procurement plan
|
||||
*/
|
||||
export const useCurrentProcurementPlan = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementPlanResponse | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementPlanResponse | null, ApiError>({
|
||||
queryKey: procurementKeys.currentPlan(tenantId),
|
||||
queryFn: () => ProcurementService.getCurrentProcurementPlan(tenantId),
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// PROCUREMENT REQUIREMENTS QUERIES
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get requirements for a specific procurement plan
|
||||
*/
|
||||
export const usePlanRequirements = (
|
||||
params: GetPlanRequirementsParams,
|
||||
options?: Omit<UseQueryOptions<ProcurementRequirementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementRequirementResponse[], ApiError>({
|
||||
queryKey: procurementKeys.planRequirements(params),
|
||||
queryFn: () => ProcurementService.getPlanRequirements(params),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!params.tenant_id && !!params.plan_id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get critical requirements across all plans
|
||||
*/
|
||||
export const useCriticalRequirements = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<ProcurementRequirementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<ProcurementRequirementResponse[], ApiError>({
|
||||
queryKey: procurementKeys.criticalRequirements(tenantId),
|
||||
queryFn: () => ProcurementService.getCriticalRequirements(tenantId),
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
enabled: !!tenantId,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// PROCUREMENT PLAN MUTATIONS
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Generate a new procurement plan (manual/UI-driven)
|
||||
*/
|
||||
export const useGenerateProcurementPlan = (
|
||||
options?: UseMutationOptions<GeneratePlanResponse, ApiError, { tenantId: string; request: GeneratePlanRequest }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GeneratePlanResponse, ApiError, { tenantId: string; request: GeneratePlanRequest }>({
|
||||
mutationFn: ({ tenantId, request }) => ProcurementService.generateProcurementPlan(tenantId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all procurement queries for this tenant
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.all,
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
|
||||
// If plan was generated successfully, cache it
|
||||
if (data.success && data.plan) {
|
||||
queryClient.setQueryData(procurementKeys.plan(variables.tenantId, data.plan.id), data.plan);
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto-generate procurement plan from forecast data (Orchestrator integration)
|
||||
*/
|
||||
export const useAutoGenerateProcurement = (
|
||||
options?: UseMutationOptions<
|
||||
AutoGenerateProcurementResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; request: AutoGenerateProcurementRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
AutoGenerateProcurementResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; request: AutoGenerateProcurementRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, request }) => ProcurementService.autoGenerateProcurement(tenantId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all procurement queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.all,
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
|
||||
// If plan was created successfully, cache it
|
||||
if (data.success && data.plan_id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.currentPlan(variables.tenantId),
|
||||
});
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update procurement plan status
|
||||
*/
|
||||
export const useUpdateProcurementPlanStatus = (
|
||||
options?: UseMutationOptions<ProcurementPlanResponse, ApiError, UpdatePlanStatusParams>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ProcurementPlanResponse, ApiError, UpdatePlanStatusParams>({
|
||||
mutationFn: (params) => ProcurementService.updateProcurementPlanStatus(params),
|
||||
onSuccess: (data, variables) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(procurementKeys.plan(variables.tenant_id, variables.plan_id), data);
|
||||
|
||||
// Invalidate plans list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.plans(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenant_id);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Recalculate an existing procurement plan
|
||||
*/
|
||||
export const useRecalculateProcurementPlan = (
|
||||
options?: UseMutationOptions<GeneratePlanResponse, ApiError, { tenantId: string; planId: string }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GeneratePlanResponse, ApiError, { tenantId: string; planId: string }>({
|
||||
mutationFn: ({ tenantId, planId }) => ProcurementService.recalculateProcurementPlan(tenantId, planId),
|
||||
onSuccess: (data, variables) => {
|
||||
if (data.plan) {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(procurementKeys.plan(variables.tenantId, variables.planId), data.plan);
|
||||
}
|
||||
|
||||
// Invalidate plans list and dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.all,
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Approve a procurement plan
|
||||
*/
|
||||
export const useApproveProcurementPlan = (
|
||||
options?: UseMutationOptions<
|
||||
ProcurementPlanResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; planId: string; approval_notes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
ProcurementPlanResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; planId: string; approval_notes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, planId, approval_notes }) =>
|
||||
ProcurementService.approveProcurementPlan(tenantId, planId, { approval_notes }),
|
||||
onSuccess: (data, variables) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(procurementKeys.plan(variables.tenantId, variables.planId), data);
|
||||
|
||||
// Invalidate plans list and dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.all,
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reject a procurement plan
|
||||
*/
|
||||
export const useRejectProcurementPlan = (
|
||||
options?: UseMutationOptions<
|
||||
ProcurementPlanResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; planId: string; rejection_notes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
ProcurementPlanResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; planId: string; rejection_notes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, planId, rejection_notes }) =>
|
||||
ProcurementService.rejectProcurementPlan(tenantId, planId, { rejection_notes }),
|
||||
onSuccess: (data, variables) => {
|
||||
// Update the specific plan in cache
|
||||
queryClient.setQueryData(procurementKeys.plan(variables.tenantId, variables.planId), data);
|
||||
|
||||
// Invalidate plans list and dashboard
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.all,
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// PURCHASE ORDER MUTATIONS
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Create purchase orders from procurement plan
|
||||
*/
|
||||
export const useCreatePurchaseOrdersFromPlan = (
|
||||
options?: UseMutationOptions<CreatePOsResult, ApiError, { tenantId: string; planId: string; autoApprove?: boolean }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreatePOsResult, ApiError, { tenantId: string; planId: string; autoApprove?: boolean }>({
|
||||
mutationFn: ({ tenantId, planId, autoApprove = false }) =>
|
||||
ProcurementService.createPurchaseOrdersFromPlan(tenantId, planId, autoApprove),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate procurement plan to refresh requirements status
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.plan(variables.tenantId, variables.planId),
|
||||
});
|
||||
|
||||
// Invalidate plan requirements
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.requirements(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.planId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Link a procurement requirement to a purchase order
|
||||
*/
|
||||
export const useLinkRequirementToPurchaseOrder = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string; requirement_id: string; purchase_order_id: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: LinkRequirementToPORequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ success: boolean; message: string; requirement_id: string; purchase_order_id: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: LinkRequirementToPORequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, requirementId, request }) =>
|
||||
ProcurementService.linkRequirementToPurchaseOrder(tenantId, requirementId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate procurement data to refresh requirements
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.requirements(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update delivery status for a requirement
|
||||
*/
|
||||
export const useUpdateRequirementDeliveryStatus = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string; requirement_id: string; delivery_status: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ success: boolean; message: string; requirement_id: string; delivery_status: string },
|
||||
ApiError,
|
||||
{ tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest }
|
||||
>({
|
||||
mutationFn: ({ tenantId, requirementId, request }) =>
|
||||
ProcurementService.updateRequirementDeliveryStatus(tenantId, requirementId, request),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate procurement data to refresh requirements
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: procurementKeys.requirements(),
|
||||
predicate: (query) => {
|
||||
return JSON.stringify(query.queryKey).includes(variables.tenantId);
|
||||
},
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export const useSubscription = () => {
|
||||
error: 'Failed to load subscription data'
|
||||
}));
|
||||
}
|
||||
}, [tenantId, notifySubscriptionChanged]);
|
||||
}, [tenantId]); // Removed notifySubscriptionChanged - it's now stable from context
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscriptionData();
|
||||
|
||||
@@ -319,16 +319,16 @@ export const useUpdateMemberRole = (
|
||||
|
||||
export const useRemoveTeamMember = (
|
||||
options?: UseMutationOptions<
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; memberUserId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
return useMutation<
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ success: boolean; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; memberUserId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, memberUserId }) => tenantService.removeTeamMember(tenantId, memberUserId),
|
||||
@@ -338,4 +338,36 @@ export const useRemoveTeamMember = (
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to transfer tenant ownership to another admin
|
||||
* This is a critical operation that changes the tenant owner
|
||||
*/
|
||||
export const useTransferOwnership = (
|
||||
options?: UseMutationOptions<
|
||||
TenantResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; newOwnerId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
TenantResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; newOwnerId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, newOwnerId }) => tenantService.transferOwnership(tenantId, newOwnerId),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate all tenant-related queries since ownership changed
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.detail(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') });
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') });
|
||||
// Invalidate access queries for all users since roles changed
|
||||
queryClient.invalidateQueries({ queryKey: tenantKeys.access(tenantId, '') });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user