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,
|
||||
});
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export { posService } from './services/pos';
|
||||
export { recipesService } from './services/recipes';
|
||||
|
||||
// NEW: Sprint 2 & 3 Services
|
||||
export * as procurementService from './services/procurement';
|
||||
export { ProcurementService } from './services/procurement-service';
|
||||
export * as orchestratorService from './services/orchestrator';
|
||||
|
||||
// Types - Auth
|
||||
@@ -289,31 +289,55 @@ export type {
|
||||
GetCustomersParams,
|
||||
UpdateOrderStatusParams,
|
||||
GetDemandRequirementsParams,
|
||||
// Procurement types
|
||||
} from './types/orders';
|
||||
|
||||
// Types - Procurement
|
||||
export type {
|
||||
// Enums
|
||||
ProcurementPlanType,
|
||||
ProcurementStrategy,
|
||||
RiskLevel,
|
||||
RequirementStatus,
|
||||
PlanStatus,
|
||||
DeliveryStatus,
|
||||
DeliveryStatus as ProcurementDeliveryStatus,
|
||||
PriorityLevel as ProcurementPriorityLevel,
|
||||
BusinessModel as ProcurementBusinessModel,
|
||||
|
||||
// Requirement types
|
||||
ProcurementRequirementBase,
|
||||
ProcurementRequirementCreate,
|
||||
ProcurementRequirementUpdate,
|
||||
ProcurementRequirementResponse,
|
||||
|
||||
// Plan types
|
||||
ProcurementPlanBase,
|
||||
ProcurementPlanCreate,
|
||||
ProcurementPlanUpdate,
|
||||
ProcurementPlanResponse,
|
||||
ApprovalWorkflowEntry,
|
||||
|
||||
// Dashboard & Analytics
|
||||
ProcurementSummary,
|
||||
ProcurementDashboardData,
|
||||
|
||||
// Request/Response types
|
||||
GeneratePlanRequest,
|
||||
GeneratePlanResponse,
|
||||
AutoGenerateProcurementRequest,
|
||||
AutoGenerateProcurementResponse,
|
||||
CreatePOsResult,
|
||||
LinkRequirementToPORequest,
|
||||
UpdateDeliveryStatusRequest,
|
||||
ApprovalRequest,
|
||||
RejectionRequest,
|
||||
PaginatedProcurementPlans,
|
||||
ForecastRequest,
|
||||
ForecastRequest as ProcurementForecastRequest,
|
||||
|
||||
// Query params
|
||||
GetProcurementPlansParams,
|
||||
GetPlanRequirementsParams,
|
||||
UpdatePlanStatusParams,
|
||||
} from './types/orders';
|
||||
} from './types/procurement';
|
||||
|
||||
// Types - Forecasting
|
||||
export type {
|
||||
@@ -609,26 +633,34 @@ export {
|
||||
useCreateCustomer,
|
||||
useUpdateCustomer,
|
||||
useInvalidateOrders,
|
||||
// Procurement hooks
|
||||
ordersKeys,
|
||||
} from './hooks/orders';
|
||||
|
||||
// Hooks - Procurement
|
||||
export {
|
||||
// Queries
|
||||
useProcurementDashboard,
|
||||
useProcurementPlans,
|
||||
useProcurementPlan,
|
||||
useProcurementPlanByDate,
|
||||
useCurrentProcurementPlan,
|
||||
useProcurementDashboard,
|
||||
usePlanRequirements,
|
||||
useCriticalRequirements,
|
||||
useProcurementHealth,
|
||||
|
||||
// Mutations
|
||||
useGenerateProcurementPlan,
|
||||
useAutoGenerateProcurement,
|
||||
useUpdateProcurementPlanStatus,
|
||||
useTriggerDailyScheduler,
|
||||
useRecalculateProcurementPlan,
|
||||
useApproveProcurementPlan,
|
||||
useRejectProcurementPlan,
|
||||
useCreatePurchaseOrdersFromPlan,
|
||||
useLinkRequirementToPurchaseOrder,
|
||||
useUpdateRequirementDeliveryStatus,
|
||||
ordersKeys,
|
||||
} from './hooks/orders';
|
||||
|
||||
// Query keys
|
||||
procurementKeys,
|
||||
} from './hooks/procurement';
|
||||
|
||||
// Hooks - Forecasting
|
||||
export {
|
||||
|
||||
@@ -28,24 +28,6 @@ import {
|
||||
GetCustomersParams,
|
||||
UpdateOrderStatusParams,
|
||||
GetDemandRequirementsParams,
|
||||
// Procurement types
|
||||
ProcurementPlanResponse,
|
||||
ProcurementPlanCreate,
|
||||
ProcurementPlanUpdate,
|
||||
ProcurementRequirementResponse,
|
||||
ProcurementRequirementUpdate,
|
||||
ProcurementDashboardData,
|
||||
GeneratePlanRequest,
|
||||
GeneratePlanResponse,
|
||||
PaginatedProcurementPlans,
|
||||
GetProcurementPlansParams,
|
||||
GetPlanRequirementsParams,
|
||||
UpdatePlanStatusParams,
|
||||
CreatePOsResult,
|
||||
LinkRequirementToPORequest,
|
||||
UpdateDeliveryStatusRequest,
|
||||
ApprovalRequest,
|
||||
RejectionRequest,
|
||||
} from '../types/orders';
|
||||
|
||||
export class OrdersService {
|
||||
@@ -209,209 +191,6 @@ export class OrdersService {
|
||||
return apiClient.get<ServiceStatus>(`/tenants/${tenantId}/orders/operations/status`);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Procurement Planning
|
||||
// Backend: services/orders/app/api/procurement_operations.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get current procurement plan for today
|
||||
* GET /tenants/{tenant_id}/orders/operations/procurement/plans/current
|
||||
*/
|
||||
static async getCurrentProcurementPlan(tenantId: string): Promise<ProcurementPlanResponse | null> {
|
||||
return apiClient.get<ProcurementPlanResponse | null>(`/tenants/${tenantId}/orders/operations/procurement/plans/current`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement plan by specific date
|
||||
* GET /tenants/{tenant_id}/orders/operations/procurement/plans/date/{plan_date}
|
||||
*/
|
||||
static async getProcurementPlanByDate(tenantId: string, planDate: string): Promise<ProcurementPlanResponse | null> {
|
||||
return apiClient.get<ProcurementPlanResponse | null>(`/tenants/${tenantId}/orders/operations/procurement/plans/date/${planDate}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement plan by ID
|
||||
* GET /tenants/{tenant_id}/orders/operations/procurement/plans/id/{plan_id}
|
||||
*/
|
||||
static async getProcurementPlanById(tenantId: string, planId: string): Promise<ProcurementPlanResponse | null> {
|
||||
return apiClient.get<ProcurementPlanResponse | null>(`/tenants/${tenantId}/orders/operations/procurement/plans/id/${planId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List procurement plans with filtering
|
||||
* GET /tenants/{tenant_id}/orders/operations/procurement/plans/
|
||||
*/
|
||||
static async getProcurementPlans(params: GetProcurementPlansParams): Promise<PaginatedProcurementPlans> {
|
||||
const { tenant_id, status, start_date, end_date, limit = 50, offset = 0 } = params;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
limit: limit.toString(),
|
||||
offset: offset.toString(),
|
||||
});
|
||||
|
||||
if (status) queryParams.append('status', status);
|
||||
if (start_date) queryParams.append('start_date', start_date);
|
||||
if (end_date) queryParams.append('end_date', end_date);
|
||||
|
||||
return apiClient.get<PaginatedProcurementPlans>(
|
||||
`/tenants/${tenant_id}/orders/operations/procurement/plans?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new procurement plan
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/plans/generate
|
||||
*/
|
||||
static async generateProcurementPlan(tenantId: string, request: GeneratePlanRequest): Promise<GeneratePlanResponse> {
|
||||
return apiClient.post<GeneratePlanResponse>(`/tenants/${tenantId}/orders/operations/procurement/plans/generate`, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update procurement plan status
|
||||
* PUT /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/status
|
||||
*/
|
||||
static async updateProcurementPlanStatus(params: UpdatePlanStatusParams): Promise<ProcurementPlanResponse> {
|
||||
const { tenant_id, plan_id, status } = params;
|
||||
|
||||
const queryParams = new URLSearchParams({ status });
|
||||
|
||||
return apiClient.put<ProcurementPlanResponse>(
|
||||
`/tenants/${tenant_id}/orders/operations/procurement/plans/${plan_id}/status?${queryParams.toString()}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement dashboard data
|
||||
* GET /tenants/{tenant_id}/orders/dashboard/procurement
|
||||
*/
|
||||
static async getProcurementDashboard(tenantId: string): Promise<ProcurementDashboardData | null> {
|
||||
return apiClient.get<ProcurementDashboardData | null>(`/tenants/${tenantId}/orders/dashboard/procurement`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get requirements for a specific plan
|
||||
* GET /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/requirements
|
||||
*/
|
||||
static async getPlanRequirements(params: GetPlanRequirementsParams): Promise<ProcurementRequirementResponse[]> {
|
||||
const { tenant_id, plan_id, status, priority } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
if (status) queryParams.append('status', status);
|
||||
if (priority) queryParams.append('priority', priority);
|
||||
|
||||
const url = `/tenants/${tenant_id}/orders/operations/procurement/plans/${plan_id}/requirements${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
||||
|
||||
return apiClient.get<ProcurementRequirementResponse[]>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get critical requirements across all plans
|
||||
* GET /tenants/{tenant_id}/orders/operations/procurement/requirements/critical
|
||||
*/
|
||||
static async getCriticalRequirements(tenantId: string): Promise<ProcurementRequirementResponse[]> {
|
||||
return apiClient.get<ProcurementRequirementResponse[]>(`/tenants/${tenantId}/orders/operations/procurement/requirements/critical`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger daily scheduler manually
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/scheduler/trigger
|
||||
*/
|
||||
static async triggerDailyScheduler(tenantId: string): Promise<{ success: boolean; message: string; tenant_id: string }> {
|
||||
return apiClient.post<{ success: boolean; message: string; tenant_id: string }>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/scheduler/trigger`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement service health
|
||||
* GET /tenants/{tenant_id}/orders/base/procurement/health
|
||||
*/
|
||||
static async getProcurementHealth(tenantId: string): Promise<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }> {
|
||||
return apiClient.get<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }>(`/tenants/${tenantId}/orders/base/procurement/health`);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Advanced Procurement Features
|
||||
// Backend: services/orders/app/api/procurement_operations.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Recalculate an existing procurement plan
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/recalculate
|
||||
*/
|
||||
static async recalculateProcurementPlan(tenantId: string, planId: string): Promise<GeneratePlanResponse> {
|
||||
return apiClient.post<GeneratePlanResponse>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/recalculate`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a procurement plan with notes
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/approve
|
||||
*/
|
||||
static async approveProcurementPlan(tenantId: string, planId: string, request?: ApprovalRequest): Promise<ProcurementPlanResponse> {
|
||||
return apiClient.post<ProcurementPlanResponse>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/approve`,
|
||||
request || {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a procurement plan with notes
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/reject
|
||||
*/
|
||||
static async rejectProcurementPlan(tenantId: string, planId: string, request?: RejectionRequest): Promise<ProcurementPlanResponse> {
|
||||
return apiClient.post<ProcurementPlanResponse>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/reject`,
|
||||
request || {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create purchase orders automatically from procurement plan
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/create-purchase-orders
|
||||
*/
|
||||
static async createPurchaseOrdersFromPlan(tenantId: string, planId: string, autoApprove: boolean = false): Promise<CreatePOsResult> {
|
||||
return apiClient.post<CreatePOsResult>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/create-purchase-orders`,
|
||||
{ auto_approve: autoApprove }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a procurement requirement to a purchase order
|
||||
* POST /tenants/{tenant_id}/orders/operations/procurement/requirements/{requirement_id}/link-purchase-order
|
||||
*/
|
||||
static async linkRequirementToPurchaseOrder(
|
||||
tenantId: string,
|
||||
requirementId: string,
|
||||
request: LinkRequirementToPORequest
|
||||
): Promise<{ success: boolean; message: string; requirement_id: string; purchase_order_id: string }> {
|
||||
return apiClient.post<{ success: boolean; message: string; requirement_id: string; purchase_order_id: string }>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/requirements/${requirementId}/link-purchase-order`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update delivery status for a requirement
|
||||
* PUT /tenants/{tenant_id}/orders/operations/procurement/requirements/{requirement_id}/delivery-status
|
||||
*/
|
||||
static async updateRequirementDeliveryStatus(
|
||||
tenantId: string,
|
||||
requirementId: string,
|
||||
request: UpdateDeliveryStatusRequest
|
||||
): Promise<{ success: boolean; message: string; requirement_id: string; delivery_status: string }> {
|
||||
return apiClient.put<{ success: boolean; message: string; requirement_id: string; delivery_status: string }>(
|
||||
`/tenants/${tenantId}/orders/operations/procurement/requirements/${requirementId}/delivery-status`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OrdersService;
|
||||
|
||||
335
frontend/src/api/services/procurement-service.ts
Normal file
335
frontend/src/api/services/procurement-service.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/services/procurement-service.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* Procurement Service - Fully aligned with backend Procurement Service API
|
||||
*
|
||||
* Backend API: services/procurement/app/api/
|
||||
* - procurement_plans.py: Plan CRUD and generation
|
||||
* - analytics.py: Analytics and dashboard
|
||||
* - purchase_orders.py: PO creation from plans
|
||||
*
|
||||
* Base URL: /api/v1/tenants/{tenant_id}/procurement/*
|
||||
*
|
||||
* Last Updated: 2025-10-31
|
||||
* Status: ✅ Complete - 100% backend alignment
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client/apiClient';
|
||||
import {
|
||||
// Procurement Plan types
|
||||
ProcurementPlanResponse,
|
||||
ProcurementPlanCreate,
|
||||
ProcurementPlanUpdate,
|
||||
PaginatedProcurementPlans,
|
||||
|
||||
// Procurement Requirement types
|
||||
ProcurementRequirementResponse,
|
||||
ProcurementRequirementUpdate,
|
||||
|
||||
// Dashboard & Analytics types
|
||||
ProcurementDashboardData,
|
||||
ProcurementTrendsData,
|
||||
|
||||
// Request/Response types
|
||||
GeneratePlanRequest,
|
||||
GeneratePlanResponse,
|
||||
AutoGenerateProcurementRequest,
|
||||
AutoGenerateProcurementResponse,
|
||||
CreatePOsResult,
|
||||
LinkRequirementToPORequest,
|
||||
UpdateDeliveryStatusRequest,
|
||||
ApprovalRequest,
|
||||
RejectionRequest,
|
||||
|
||||
// Query parameter types
|
||||
GetProcurementPlansParams,
|
||||
GetPlanRequirementsParams,
|
||||
UpdatePlanStatusParams,
|
||||
} from '../types/procurement';
|
||||
|
||||
/**
|
||||
* Procurement Service
|
||||
* All methods use the standalone Procurement Service backend API
|
||||
*/
|
||||
export class ProcurementService {
|
||||
|
||||
// ===================================================================
|
||||
// ANALYTICS & DASHBOARD
|
||||
// Backend: services/procurement/app/api/analytics.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get procurement analytics dashboard data
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement
|
||||
*/
|
||||
static async getProcurementAnalytics(tenantId: string): Promise<ProcurementDashboardData> {
|
||||
return apiClient.get<ProcurementDashboardData>(`/tenants/${tenantId}/procurement/analytics/procurement`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement time-series trends for charts
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement/trends
|
||||
*/
|
||||
static async getProcurementTrends(tenantId: string, days: number = 7): Promise<ProcurementTrendsData> {
|
||||
return apiClient.get<ProcurementTrendsData>(`/tenants/${tenantId}/procurement/analytics/procurement/trends?days=${days}`);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// PROCUREMENT PLAN GENERATION
|
||||
// Backend: services/procurement/app/api/procurement_plans.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Auto-generate procurement plan from forecast data (Orchestrator integration)
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/auto-generate
|
||||
*
|
||||
* Called by Orchestrator Service to create procurement plans based on forecast data
|
||||
*/
|
||||
static async autoGenerateProcurement(
|
||||
tenantId: string,
|
||||
request: AutoGenerateProcurementRequest
|
||||
): Promise<AutoGenerateProcurementResponse> {
|
||||
return apiClient.post<AutoGenerateProcurementResponse>(
|
||||
`/tenants/${tenantId}/procurement/auto-generate`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new procurement plan (manual/UI-driven)
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans/generate
|
||||
*/
|
||||
static async generateProcurementPlan(
|
||||
tenantId: string,
|
||||
request: GeneratePlanRequest
|
||||
): Promise<GeneratePlanResponse> {
|
||||
return apiClient.post<GeneratePlanResponse>(
|
||||
`/tenants/${tenantId}/procurement/plans/generate`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// PROCUREMENT PLAN CRUD
|
||||
// Backend: services/procurement/app/api/procurement_plans.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get the current day's procurement plan
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/plans/current
|
||||
*/
|
||||
static async getCurrentProcurementPlan(tenantId: string): Promise<ProcurementPlanResponse | null> {
|
||||
return apiClient.get<ProcurementPlanResponse | null>(
|
||||
`/tenants/${tenantId}/procurement/plans/current`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement plan by ID
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}
|
||||
*/
|
||||
static async getProcurementPlanById(
|
||||
tenantId: string,
|
||||
planId: string
|
||||
): Promise<ProcurementPlanResponse | null> {
|
||||
return apiClient.get<ProcurementPlanResponse | null>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get procurement plan for a specific date
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/plans/date/{plan_date}
|
||||
*/
|
||||
static async getProcurementPlanByDate(
|
||||
tenantId: string,
|
||||
planDate: string
|
||||
): Promise<ProcurementPlanResponse | null> {
|
||||
return apiClient.get<ProcurementPlanResponse | null>(
|
||||
`/tenants/${tenantId}/procurement/plans/date/${planDate}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all procurement plans for tenant with pagination and filtering
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/plans
|
||||
*/
|
||||
static async getProcurementPlans(params: GetProcurementPlansParams): Promise<PaginatedProcurementPlans> {
|
||||
const { tenant_id, status, start_date, end_date, limit = 50, offset = 0 } = params;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
limit: limit.toString(),
|
||||
offset: offset.toString(),
|
||||
});
|
||||
|
||||
if (status) queryParams.append('status', status);
|
||||
if (start_date) queryParams.append('start_date', start_date);
|
||||
if (end_date) queryParams.append('end_date', end_date);
|
||||
|
||||
return apiClient.get<PaginatedProcurementPlans>(
|
||||
`/tenants/${tenant_id}/procurement/plans?${queryParams.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update procurement plan status
|
||||
* PATCH /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/status
|
||||
*/
|
||||
static async updateProcurementPlanStatus(params: UpdatePlanStatusParams): Promise<ProcurementPlanResponse> {
|
||||
const { tenant_id, plan_id, status, notes } = params;
|
||||
|
||||
const queryParams = new URLSearchParams({ status });
|
||||
if (notes) queryParams.append('notes', notes);
|
||||
|
||||
return apiClient.patch<ProcurementPlanResponse>(
|
||||
`/tenants/${tenant_id}/procurement/plans/${plan_id}/status?${queryParams.toString()}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// PROCUREMENT REQUIREMENTS
|
||||
// Backend: services/procurement/app/api/procurement_plans.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Get all requirements for a procurement plan
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/requirements
|
||||
*/
|
||||
static async getPlanRequirements(params: GetPlanRequirementsParams): Promise<ProcurementRequirementResponse[]> {
|
||||
const { tenant_id, plan_id, status, priority } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
if (status) queryParams.append('status', status);
|
||||
if (priority) queryParams.append('priority', priority);
|
||||
|
||||
const url = `/tenants/${tenant_id}/procurement/plans/${plan_id}/requirements${
|
||||
queryParams.toString() ? `?${queryParams.toString()}` : ''
|
||||
}`;
|
||||
|
||||
return apiClient.get<ProcurementRequirementResponse[]>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get critical requirements across all plans
|
||||
* GET /api/v1/tenants/{tenant_id}/procurement/requirements/critical
|
||||
*/
|
||||
static async getCriticalRequirements(tenantId: string): Promise<ProcurementRequirementResponse[]> {
|
||||
return apiClient.get<ProcurementRequirementResponse[]>(
|
||||
`/tenants/${tenantId}/procurement/requirements/critical`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a procurement requirement to a purchase order
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/requirements/{requirement_id}/link-purchase-order
|
||||
*/
|
||||
static async linkRequirementToPurchaseOrder(
|
||||
tenantId: string,
|
||||
requirementId: string,
|
||||
request: LinkRequirementToPORequest
|
||||
): Promise<{ success: boolean; message: string; requirement_id: string; purchase_order_id: string }> {
|
||||
return apiClient.post<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
requirement_id: string;
|
||||
purchase_order_id: string;
|
||||
}>(
|
||||
`/tenants/${tenantId}/procurement/requirements/${requirementId}/link-purchase-order`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update delivery status for a requirement
|
||||
* PUT /api/v1/tenants/{tenant_id}/procurement/requirements/{requirement_id}/delivery-status
|
||||
*/
|
||||
static async updateRequirementDeliveryStatus(
|
||||
tenantId: string,
|
||||
requirementId: string,
|
||||
request: UpdateDeliveryStatusRequest
|
||||
): Promise<{ success: boolean; message: string; requirement_id: string; delivery_status: string }> {
|
||||
return apiClient.put<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
requirement_id: string;
|
||||
delivery_status: string;
|
||||
}>(
|
||||
`/tenants/${tenantId}/procurement/requirements/${requirementId}/delivery-status`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ADVANCED PROCUREMENT OPERATIONS
|
||||
// Backend: services/procurement/app/api/procurement_plans.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Recalculate an existing procurement plan
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/recalculate
|
||||
*/
|
||||
static async recalculateProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string
|
||||
): Promise<GeneratePlanResponse> {
|
||||
return apiClient.post<GeneratePlanResponse>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/recalculate`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a procurement plan with optional notes
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/approve
|
||||
*/
|
||||
static async approveProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
request?: ApprovalRequest
|
||||
): Promise<ProcurementPlanResponse> {
|
||||
return apiClient.post<ProcurementPlanResponse>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/approve`,
|
||||
request || {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a procurement plan with optional notes
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/reject
|
||||
*/
|
||||
static async rejectProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
request?: RejectionRequest
|
||||
): Promise<ProcurementPlanResponse> {
|
||||
return apiClient.post<ProcurementPlanResponse>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/reject`,
|
||||
request || {}
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// PURCHASE ORDERS
|
||||
// Backend: services/procurement/app/api/purchase_orders.py
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Create purchase orders from procurement plan requirements
|
||||
* Groups requirements by supplier and creates POs
|
||||
* POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/create-purchase-orders
|
||||
*/
|
||||
static async createPurchaseOrdersFromPlan(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
autoApprove: boolean = false
|
||||
): Promise<CreatePOsResult> {
|
||||
return apiClient.post<CreatePOsResult>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/create-purchase-orders`,
|
||||
{ auto_approve: autoApprove }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProcurementService;
|
||||
@@ -1,317 +0,0 @@
|
||||
/**
|
||||
* Procurement Service API Client
|
||||
* Handles procurement planning and purchase order management
|
||||
*
|
||||
* NEW in Sprint 3: Procurement Service now owns all procurement operations
|
||||
* Previously these were split between Orders Service and Suppliers Service
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
|
||||
// ============================================================================
|
||||
// PROCUREMENT PLAN TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface ProcurementRequirement {
|
||||
id: string;
|
||||
ingredient_id: string;
|
||||
ingredient_name?: string;
|
||||
ingredient_sku?: string;
|
||||
required_quantity: number;
|
||||
current_stock: number;
|
||||
quantity_to_order: number;
|
||||
unit_of_measure: string;
|
||||
estimated_cost: string; // Decimal as string
|
||||
priority: 'urgent' | 'high' | 'normal' | 'low';
|
||||
reason: string;
|
||||
supplier_id?: string;
|
||||
supplier_name?: string;
|
||||
expected_delivery_date?: string;
|
||||
// NEW: Local production support
|
||||
is_locally_produced?: boolean;
|
||||
recipe_id?: string;
|
||||
parent_requirement_id?: string;
|
||||
bom_explosion_level?: number;
|
||||
}
|
||||
|
||||
export interface ProcurementPlanSummary {
|
||||
id: string;
|
||||
plan_date: string;
|
||||
status: 'DRAFT' | 'PENDING_APPROVAL' | 'APPROVED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
|
||||
total_requirements: number;
|
||||
total_estimated_cost: string; // Decimal as string
|
||||
planning_horizon_days: number;
|
||||
auto_generated: boolean;
|
||||
// NEW: Orchestrator integration
|
||||
forecast_id?: string;
|
||||
production_schedule_id?: string;
|
||||
created_at: string;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface ProcurementPlanDetail extends ProcurementPlanSummary {
|
||||
requirements: ProcurementRequirement[];
|
||||
notes?: string;
|
||||
approved_by?: string;
|
||||
approved_at?: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTO-GENERATE PROCUREMENT TYPES (Orchestrator Integration)
|
||||
// ============================================================================
|
||||
|
||||
export interface AutoGenerateProcurementRequest {
|
||||
forecast_data: Record<string, any>; // From Forecasting Service
|
||||
production_schedule_id?: string;
|
||||
target_date?: string; // YYYY-MM-DD
|
||||
planning_horizon_days?: number; // Default: 14
|
||||
safety_stock_percentage?: number; // Default: 20.00
|
||||
auto_create_pos?: boolean; // Default: true
|
||||
auto_approve_pos?: boolean; // Default: false
|
||||
}
|
||||
|
||||
export interface AutoGenerateProcurementResponse {
|
||||
success: boolean;
|
||||
plan?: ProcurementPlanDetail;
|
||||
purchase_orders_created?: number;
|
||||
purchase_orders_auto_approved?: number;
|
||||
purchase_orders_pending_approval?: number;
|
||||
recipe_explosion_applied?: boolean;
|
||||
recipe_explosion_metadata?: {
|
||||
total_requirements_before: number;
|
||||
total_requirements_after: number;
|
||||
explosion_levels: number;
|
||||
locally_produced_ingredients: number;
|
||||
};
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
execution_time_ms?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROCUREMENT PLAN API FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get list of procurement plans with optional filters
|
||||
*/
|
||||
export async function listProcurementPlans(
|
||||
tenantId: string,
|
||||
params?: {
|
||||
status?: ProcurementPlanSummary['status'];
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
): Promise<ProcurementPlanSummary[]> {
|
||||
return apiClient.get<ProcurementPlanSummary[]>(
|
||||
`/tenants/${tenantId}/procurement/plans`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single procurement plan by ID with full details
|
||||
*/
|
||||
export async function getProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string
|
||||
): Promise<ProcurementPlanDetail> {
|
||||
return apiClient.get<ProcurementPlanDetail>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new procurement plan (manual)
|
||||
*/
|
||||
export async function createProcurementPlan(
|
||||
tenantId: string,
|
||||
data: {
|
||||
plan_date: string;
|
||||
planning_horizon_days?: number;
|
||||
include_safety_stock?: boolean;
|
||||
safety_stock_percentage?: number;
|
||||
notes?: string;
|
||||
}
|
||||
): Promise<ProcurementPlanDetail> {
|
||||
return apiClient.post<ProcurementPlanDetail>(
|
||||
`/tenants/${tenantId}/procurement/plans`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update procurement plan
|
||||
*/
|
||||
export async function updateProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
data: {
|
||||
status?: ProcurementPlanSummary['status'];
|
||||
notes?: string;
|
||||
}
|
||||
): Promise<ProcurementPlanDetail> {
|
||||
return apiClient.put<ProcurementPlanDetail>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete procurement plan
|
||||
*/
|
||||
export async function deleteProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string
|
||||
): Promise<{ message: string }> {
|
||||
return apiClient.delete<{ message: string }>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve procurement plan
|
||||
*/
|
||||
export async function approveProcurementPlan(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
notes?: string
|
||||
): Promise<ProcurementPlanDetail> {
|
||||
return apiClient.post<ProcurementPlanDetail>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/approve`,
|
||||
{ notes }
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTO-GENERATE PROCUREMENT (ORCHESTRATOR INTEGRATION)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Auto-generate procurement plan from forecast data
|
||||
* This is the main entry point for orchestrated procurement planning
|
||||
*
|
||||
* NEW in Sprint 3: Called by Orchestrator Service to create procurement plans
|
||||
* based on forecast data and production schedules
|
||||
*
|
||||
* Features:
|
||||
* - Receives forecast data from Forecasting Service (via Orchestrator)
|
||||
* - Calculates procurement requirements using smart calculator
|
||||
* - Applies Recipe Explosion for locally-produced ingredients
|
||||
* - Optionally creates purchase orders
|
||||
* - Optionally auto-approves qualifying POs
|
||||
*/
|
||||
export async function autoGenerateProcurement(
|
||||
tenantId: string,
|
||||
request: AutoGenerateProcurementRequest
|
||||
): Promise<AutoGenerateProcurementResponse> {
|
||||
return apiClient.post<AutoGenerateProcurementResponse>(
|
||||
`/tenants/${tenantId}/procurement/auto-generate`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test auto-generate with sample forecast data (for development/testing)
|
||||
*/
|
||||
export async function testAutoGenerateProcurement(
|
||||
tenantId: string,
|
||||
targetDate?: string
|
||||
): Promise<AutoGenerateProcurementResponse> {
|
||||
return apiClient.post<AutoGenerateProcurementResponse>(
|
||||
`/tenants/${tenantId}/procurement/auto-generate/test`,
|
||||
{ target_date: targetDate }
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROCUREMENT REQUIREMENTS API FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add requirement to procurement plan
|
||||
*/
|
||||
export async function addProcurementRequirement(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
requirement: {
|
||||
ingredient_id: string;
|
||||
required_quantity: number;
|
||||
quantity_to_order: number;
|
||||
priority: ProcurementRequirement['priority'];
|
||||
reason: string;
|
||||
supplier_id?: string;
|
||||
expected_delivery_date?: string;
|
||||
}
|
||||
): Promise<ProcurementRequirement> {
|
||||
return apiClient.post<ProcurementRequirement>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/requirements`,
|
||||
requirement
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update procurement requirement
|
||||
*/
|
||||
export async function updateProcurementRequirement(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
requirementId: string,
|
||||
data: {
|
||||
quantity_to_order?: number;
|
||||
priority?: ProcurementRequirement['priority'];
|
||||
supplier_id?: string;
|
||||
expected_delivery_date?: string;
|
||||
}
|
||||
): Promise<ProcurementRequirement> {
|
||||
return apiClient.put<ProcurementRequirement>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/requirements/${requirementId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete procurement requirement
|
||||
*/
|
||||
export async function deleteProcurementRequirement(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
requirementId: string
|
||||
): Promise<{ message: string }> {
|
||||
return apiClient.delete<{ message: string }>(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/requirements/${requirementId}`
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PURCHASE ORDERS FROM PLAN
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create purchase orders from procurement plan
|
||||
* Groups requirements by supplier and creates POs
|
||||
*/
|
||||
export async function createPurchaseOrdersFromPlan(
|
||||
tenantId: string,
|
||||
planId: string,
|
||||
options?: {
|
||||
auto_approve?: boolean;
|
||||
group_by_supplier?: boolean;
|
||||
delivery_date?: string;
|
||||
}
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
purchase_orders_created: number;
|
||||
purchase_orders_auto_approved?: number;
|
||||
purchase_orders_pending_approval?: number;
|
||||
purchase_order_ids: string[];
|
||||
message?: string;
|
||||
}> {
|
||||
return apiClient.post(
|
||||
`/tenants/${tenantId}/procurement/plans/${planId}/create-purchase-orders`,
|
||||
options
|
||||
);
|
||||
}
|
||||
@@ -168,6 +168,21 @@ export class TenantService {
|
||||
return apiClient.delete<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/members/${memberUserId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer tenant ownership to another admin
|
||||
* Backend: services/tenant/app/api/tenant_members.py - transfer_ownership endpoint
|
||||
*
|
||||
* @param tenantId - The tenant ID
|
||||
* @param newOwnerId - The user ID of the new owner (must be an existing admin)
|
||||
* @returns Updated tenant with new owner
|
||||
*/
|
||||
async transferOwnership(tenantId: string, newOwnerId: string): Promise<TenantResponse> {
|
||||
return apiClient.post<TenantResponse>(
|
||||
`${this.baseUrl}/${tenantId}/transfer-ownership`,
|
||||
{ new_owner_id: newOwnerId }
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Statistics & Admin
|
||||
// Backend: services/tenant/app/api/tenant_operations.py
|
||||
|
||||
@@ -365,369 +365,3 @@ export interface GetDemandRequirementsParams {
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PROCUREMENT ENUMS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Procurement plan types
|
||||
* Backend: ProcurementPlanType enum in models/enums.py (lines 104-108)
|
||||
*/
|
||||
export enum ProcurementPlanType {
|
||||
REGULAR = 'regular',
|
||||
EMERGENCY = 'emergency',
|
||||
SEASONAL = 'seasonal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement strategies
|
||||
* Backend: ProcurementStrategy enum in models/enums.py (lines 111-115)
|
||||
*/
|
||||
export enum ProcurementStrategy {
|
||||
JUST_IN_TIME = 'just_in_time',
|
||||
BULK = 'bulk',
|
||||
MIXED = 'mixed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk level classifications
|
||||
* Backend: RiskLevel enum in models/enums.py (lines 118-123)
|
||||
*/
|
||||
export enum RiskLevel {
|
||||
LOW = 'low',
|
||||
MEDIUM = 'medium',
|
||||
HIGH = 'high',
|
||||
CRITICAL = 'critical'
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement requirement status
|
||||
* Backend: RequirementStatus enum in models/enums.py (lines 126-133)
|
||||
*/
|
||||
export enum RequirementStatus {
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
ORDERED = 'ordered',
|
||||
PARTIALLY_RECEIVED = 'partially_received',
|
||||
RECEIVED = 'received',
|
||||
CANCELLED = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement plan status
|
||||
* Backend: PlanStatus enum in models/enums.py (lines 136-143)
|
||||
*/
|
||||
export enum PlanStatus {
|
||||
DRAFT = 'draft',
|
||||
PENDING_APPROVAL = 'pending_approval',
|
||||
APPROVED = 'approved',
|
||||
IN_EXECUTION = 'in_execution',
|
||||
COMPLETED = 'completed',
|
||||
CANCELLED = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery status for procurement
|
||||
* Backend: DeliveryStatus enum in models/enums.py (lines 146-151)
|
||||
*/
|
||||
export enum DeliveryStatus {
|
||||
PENDING = 'pending',
|
||||
IN_TRANSIT = 'in_transit',
|
||||
DELIVERED = 'delivered',
|
||||
DELAYED = 'delayed',
|
||||
CANCELLED = 'cancelled'
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PROCUREMENT TYPES
|
||||
// ================================================================
|
||||
|
||||
// Procurement Requirement Types
|
||||
export interface ProcurementRequirementBase {
|
||||
product_id: string;
|
||||
product_name: string;
|
||||
product_sku?: string;
|
||||
product_category?: string;
|
||||
product_type: string;
|
||||
required_quantity: number;
|
||||
unit_of_measure: string;
|
||||
safety_stock_quantity: number;
|
||||
total_quantity_needed: number;
|
||||
current_stock_level: number;
|
||||
reserved_stock: number;
|
||||
available_stock: number;
|
||||
net_requirement: number;
|
||||
order_demand: number;
|
||||
production_demand: number;
|
||||
forecast_demand: number;
|
||||
buffer_demand: number;
|
||||
required_by_date: string;
|
||||
lead_time_buffer_days: number;
|
||||
suggested_order_date: string;
|
||||
latest_order_date: string;
|
||||
priority: PriorityLevel;
|
||||
risk_level: RiskLevel;
|
||||
preferred_supplier_id?: string;
|
||||
backup_supplier_id?: string;
|
||||
supplier_name?: string;
|
||||
supplier_lead_time_days?: number;
|
||||
minimum_order_quantity?: number;
|
||||
estimated_unit_cost?: number;
|
||||
estimated_total_cost?: number;
|
||||
last_purchase_cost?: number;
|
||||
}
|
||||
|
||||
export interface ProcurementRequirementCreate extends ProcurementRequirementBase {
|
||||
special_requirements?: string;
|
||||
storage_requirements?: string;
|
||||
shelf_life_days?: number;
|
||||
quality_specifications?: Record<string, any>;
|
||||
procurement_notes?: string;
|
||||
}
|
||||
|
||||
export interface ProcurementRequirementUpdate {
|
||||
status?: RequirementStatus;
|
||||
priority?: PriorityLevel;
|
||||
approved_quantity?: number;
|
||||
approved_cost?: number;
|
||||
purchase_order_id?: string;
|
||||
purchase_order_number?: string;
|
||||
ordered_quantity?: number;
|
||||
expected_delivery_date?: string;
|
||||
actual_delivery_date?: string;
|
||||
received_quantity?: number;
|
||||
delivery_status?: DeliveryStatus;
|
||||
procurement_notes?: string;
|
||||
}
|
||||
|
||||
export interface ProcurementRequirementResponse extends ProcurementRequirementBase {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
requirement_number: string;
|
||||
status: RequirementStatus;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
purchase_order_id?: string;
|
||||
purchase_order_number?: string;
|
||||
ordered_quantity: number;
|
||||
ordered_at?: string;
|
||||
expected_delivery_date?: string;
|
||||
actual_delivery_date?: string;
|
||||
received_quantity: number;
|
||||
delivery_status: DeliveryStatus;
|
||||
fulfillment_rate?: number;
|
||||
on_time_delivery?: boolean;
|
||||
quality_rating?: number;
|
||||
approved_quantity?: number;
|
||||
approved_cost?: number;
|
||||
approved_at?: string;
|
||||
approved_by?: string;
|
||||
special_requirements?: string;
|
||||
storage_requirements?: string;
|
||||
shelf_life_days?: number;
|
||||
quality_specifications?: Record<string, any>;
|
||||
procurement_notes?: string;
|
||||
|
||||
// Smart procurement calculation metadata
|
||||
calculation_method?: string;
|
||||
ai_suggested_quantity?: number;
|
||||
adjusted_quantity?: number;
|
||||
adjustment_reason?: string;
|
||||
price_tier_applied?: Record<string, any>;
|
||||
supplier_minimum_applied?: boolean;
|
||||
storage_limit_applied?: boolean;
|
||||
reorder_rule_applied?: boolean;
|
||||
}
|
||||
|
||||
// Procurement Plan Types
|
||||
export interface ProcurementPlanBase {
|
||||
plan_date: string;
|
||||
plan_period_start: string;
|
||||
plan_period_end: string;
|
||||
planning_horizon_days: number;
|
||||
plan_type: ProcurementPlanType;
|
||||
priority: PriorityLevel;
|
||||
business_model?: BusinessModel;
|
||||
procurement_strategy: ProcurementStrategy;
|
||||
safety_stock_buffer: number;
|
||||
supply_risk_level: RiskLevel;
|
||||
demand_forecast_confidence?: number;
|
||||
seasonality_adjustment: number;
|
||||
special_requirements?: string;
|
||||
}
|
||||
|
||||
export interface ProcurementPlanCreate extends ProcurementPlanBase {
|
||||
tenant_id: string;
|
||||
requirements?: ProcurementRequirementCreate[];
|
||||
}
|
||||
|
||||
export interface ProcurementPlanUpdate {
|
||||
status?: PlanStatus;
|
||||
priority?: PriorityLevel;
|
||||
approved_at?: string;
|
||||
approved_by?: string;
|
||||
execution_started_at?: string;
|
||||
execution_completed_at?: string;
|
||||
special_requirements?: string;
|
||||
seasonal_adjustments?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ApprovalWorkflowEntry {
|
||||
timestamp: string;
|
||||
from_status: string;
|
||||
to_status: string;
|
||||
user_id?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ProcurementPlanResponse extends ProcurementPlanBase {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
plan_number: string;
|
||||
status: PlanStatus;
|
||||
total_requirements: number;
|
||||
total_estimated_cost: number;
|
||||
total_approved_cost: number;
|
||||
cost_variance: number;
|
||||
total_demand_orders: number;
|
||||
total_demand_quantity: number;
|
||||
total_production_requirements: number;
|
||||
primary_suppliers_count: number;
|
||||
backup_suppliers_count: number;
|
||||
supplier_diversification_score?: number;
|
||||
approved_at?: string;
|
||||
approved_by?: string;
|
||||
execution_started_at?: string;
|
||||
execution_completed_at?: string;
|
||||
fulfillment_rate?: number;
|
||||
on_time_delivery_rate?: number;
|
||||
cost_accuracy?: number;
|
||||
quality_score?: number;
|
||||
approval_workflow?: ApprovalWorkflowEntry[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
requirements: ProcurementRequirementResponse[];
|
||||
}
|
||||
|
||||
// Summary and Dashboard Types
|
||||
export interface ProcurementSummary {
|
||||
total_plans: number;
|
||||
active_plans: number;
|
||||
total_requirements: number;
|
||||
pending_requirements: number;
|
||||
critical_requirements: number;
|
||||
total_estimated_cost: number;
|
||||
total_approved_cost: number;
|
||||
cost_variance: number;
|
||||
average_fulfillment_rate?: number;
|
||||
average_on_time_delivery?: number;
|
||||
top_suppliers: Record<string, any>[];
|
||||
critical_items: Record<string, any>[];
|
||||
}
|
||||
|
||||
export interface ProcurementDashboardData {
|
||||
current_plan?: ProcurementPlanResponse;
|
||||
summary: ProcurementSummary;
|
||||
upcoming_deliveries: Record<string, any>[];
|
||||
overdue_requirements: Record<string, any>[];
|
||||
low_stock_alerts: Record<string, any>[];
|
||||
performance_metrics: Record<string, any>;
|
||||
}
|
||||
|
||||
// Request and Response Types
|
||||
export interface GeneratePlanRequest {
|
||||
plan_date?: string;
|
||||
force_regenerate: boolean;
|
||||
planning_horizon_days: number;
|
||||
include_safety_stock: boolean;
|
||||
safety_stock_percentage: number;
|
||||
}
|
||||
|
||||
export interface GeneratePlanResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
plan?: ProcurementPlanResponse;
|
||||
warnings: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// New Feature Types
|
||||
export interface CreatePOsResult {
|
||||
success: boolean;
|
||||
created_pos: {
|
||||
po_id: string;
|
||||
po_number: string;
|
||||
supplier_id: string;
|
||||
items_count: number;
|
||||
total_amount: number;
|
||||
}[];
|
||||
failed_pos: {
|
||||
supplier_id: string;
|
||||
error: string;
|
||||
}[];
|
||||
total_created: number;
|
||||
total_failed: number;
|
||||
}
|
||||
|
||||
export interface LinkRequirementToPORequest {
|
||||
purchase_order_id: string;
|
||||
purchase_order_number: string;
|
||||
ordered_quantity: number;
|
||||
expected_delivery_date?: string;
|
||||
}
|
||||
|
||||
export interface UpdateDeliveryStatusRequest {
|
||||
delivery_status: string;
|
||||
received_quantity?: number;
|
||||
actual_delivery_date?: string;
|
||||
quality_rating?: number;
|
||||
}
|
||||
|
||||
export interface ApprovalRequest {
|
||||
approval_notes?: string;
|
||||
}
|
||||
|
||||
export interface RejectionRequest {
|
||||
rejection_notes?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedProcurementPlans {
|
||||
plans: ProcurementPlanResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface ForecastRequest {
|
||||
target_date: string;
|
||||
horizon_days: number;
|
||||
include_confidence_intervals: boolean;
|
||||
product_ids?: string[];
|
||||
}
|
||||
|
||||
// Query Parameter Types for Procurement
|
||||
export interface GetProcurementPlansParams {
|
||||
tenant_id: string;
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface GetPlanRequirementsParams {
|
||||
tenant_id: string;
|
||||
plan_id: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePlanStatusParams {
|
||||
tenant_id: string;
|
||||
plan_id: string;
|
||||
status: PlanStatus;
|
||||
}
|
||||
192
frontend/src/api/types/performance.ts
Normal file
192
frontend/src/api/types/performance.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Performance Analytics Types
|
||||
* Comprehensive types for performance monitoring across all departments
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Overview Metrics
|
||||
// ============================================================================
|
||||
|
||||
export interface PerformanceOverview {
|
||||
overall_efficiency: number;
|
||||
average_production_time: number;
|
||||
quality_score: number;
|
||||
employee_productivity: number;
|
||||
customer_satisfaction: number;
|
||||
resource_utilization: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Department Performance
|
||||
// ============================================================================
|
||||
|
||||
export interface DepartmentPerformance {
|
||||
department_id: string;
|
||||
department_name: string;
|
||||
efficiency: number;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
metrics: DepartmentMetrics;
|
||||
}
|
||||
|
||||
export interface DepartmentMetrics {
|
||||
primary_metric: MetricValue;
|
||||
secondary_metric: MetricValue;
|
||||
tertiary_metric: MetricValue;
|
||||
}
|
||||
|
||||
export interface MetricValue {
|
||||
label: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
trend?: number;
|
||||
}
|
||||
|
||||
// Production Department
|
||||
export interface ProductionPerformance {
|
||||
efficiency: number;
|
||||
average_batch_time: number;
|
||||
quality_rate: number;
|
||||
waste_percentage: number;
|
||||
capacity_utilization: number;
|
||||
equipment_efficiency: number;
|
||||
on_time_completion_rate: number;
|
||||
yield_rate: number;
|
||||
}
|
||||
|
||||
// Inventory Department
|
||||
export interface InventoryPerformance {
|
||||
stock_accuracy: number;
|
||||
turnover_rate: number;
|
||||
waste_rate: number;
|
||||
low_stock_count: number;
|
||||
compliance_rate: number;
|
||||
expiring_items_count: number;
|
||||
stock_value: number;
|
||||
}
|
||||
|
||||
// Sales Department
|
||||
export interface SalesPerformance {
|
||||
total_revenue: number;
|
||||
total_transactions: number;
|
||||
average_transaction_value: number;
|
||||
growth_rate: number;
|
||||
channel_performance: ChannelPerformance[];
|
||||
top_products: ProductPerformance[];
|
||||
}
|
||||
|
||||
export interface ChannelPerformance {
|
||||
channel: string;
|
||||
revenue: number;
|
||||
transactions: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface ProductPerformance {
|
||||
product_id: string;
|
||||
product_name: string;
|
||||
sales: number;
|
||||
revenue: number;
|
||||
}
|
||||
|
||||
// Procurement/Administration Department
|
||||
export interface ProcurementPerformance {
|
||||
fulfillment_rate: number;
|
||||
on_time_delivery_rate: number;
|
||||
cost_accuracy: number;
|
||||
supplier_performance_score: number;
|
||||
active_plans: number;
|
||||
critical_requirements: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// KPI Tracking
|
||||
// ============================================================================
|
||||
|
||||
export interface KPIMetric {
|
||||
id: string;
|
||||
name: string;
|
||||
current_value: number;
|
||||
target_value: number;
|
||||
previous_value: number;
|
||||
unit: string;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
status: 'good' | 'warning' | 'critical';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Performance Alerts
|
||||
// ============================================================================
|
||||
|
||||
export interface PerformanceAlert {
|
||||
id: string;
|
||||
type: 'warning' | 'critical' | 'info';
|
||||
department: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
metric_affected: string;
|
||||
current_value?: number;
|
||||
threshold_value?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Time-Series Data
|
||||
// ============================================================================
|
||||
|
||||
export interface TimeSeriesData {
|
||||
timestamp: string;
|
||||
value: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface HourlyProductivity {
|
||||
hour: string;
|
||||
efficiency: number;
|
||||
production_count: number;
|
||||
sales_count: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Aggregated Dashboard Data
|
||||
// ============================================================================
|
||||
|
||||
export interface PerformanceDashboard {
|
||||
overview: PerformanceOverview;
|
||||
departments: DepartmentPerformance[];
|
||||
kpis: KPIMetric[];
|
||||
alerts: PerformanceAlert[];
|
||||
hourly_data: HourlyProductivity[];
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Filter and Query Parameters
|
||||
// ============================================================================
|
||||
|
||||
export type TimePeriod = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||
export type MetricType = 'efficiency' | 'productivity' | 'quality' | 'satisfaction';
|
||||
|
||||
export interface PerformanceFilters {
|
||||
period: TimePeriod;
|
||||
metric_type?: MetricType;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
departments?: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Trend Analysis
|
||||
// ============================================================================
|
||||
|
||||
export interface TrendData {
|
||||
date: string;
|
||||
value: number;
|
||||
comparison_value?: number;
|
||||
}
|
||||
|
||||
export interface PerformanceTrend {
|
||||
metric_name: string;
|
||||
current_period: TrendData[];
|
||||
previous_period: TrendData[];
|
||||
change_percentage: number;
|
||||
trend_direction: 'up' | 'down' | 'stable';
|
||||
}
|
||||
634
frontend/src/api/types/procurement.ts
Normal file
634
frontend/src/api/types/procurement.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* TypeScript types for Procurement Service
|
||||
* Mirrored from backend schemas: services/procurement/app/schemas/procurement_schemas.py
|
||||
* Backend enums: services/shared/app/models/enums.py
|
||||
* Backend API: services/procurement/app/api/
|
||||
*
|
||||
* Coverage:
|
||||
* - Procurement Plans (MRP-style procurement planning)
|
||||
* - Procurement Requirements (demand-driven purchasing)
|
||||
* - Purchase Orders creation from plans
|
||||
* - Analytics & Dashboard
|
||||
* - Auto-generation (Orchestrator integration)
|
||||
*/
|
||||
|
||||
// ================================================================
|
||||
// ENUMS
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Procurement plan types
|
||||
* Backend: ProcurementPlanType enum in models/enums.py
|
||||
*/
|
||||
export enum ProcurementPlanType {
|
||||
REGULAR = 'regular',
|
||||
EMERGENCY = 'emergency',
|
||||
SEASONAL = 'seasonal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement strategies
|
||||
* Backend: ProcurementStrategy enum in models/enums.py
|
||||
*/
|
||||
export enum ProcurementStrategy {
|
||||
JUST_IN_TIME = 'just_in_time',
|
||||
BULK = 'bulk',
|
||||
MIXED = 'mixed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk level classifications
|
||||
* Backend: RiskLevel enum in models/enums.py
|
||||
*/
|
||||
export enum RiskLevel {
|
||||
LOW = 'low',
|
||||
MEDIUM = 'medium',
|
||||
HIGH = 'high',
|
||||
CRITICAL = 'critical'
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement requirement status
|
||||
* Backend: RequirementStatus enum in models/enums.py
|
||||
*/
|
||||
export enum RequirementStatus {
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
ORDERED = 'ordered',
|
||||
PARTIALLY_RECEIVED = 'partially_received',
|
||||
RECEIVED = 'received',
|
||||
CANCELLED = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement plan status
|
||||
* Backend: PlanStatus enum in models/enums.py
|
||||
*/
|
||||
export enum PlanStatus {
|
||||
DRAFT = 'draft',
|
||||
PENDING_APPROVAL = 'pending_approval',
|
||||
APPROVED = 'approved',
|
||||
IN_EXECUTION = 'in_execution',
|
||||
COMPLETED = 'completed',
|
||||
CANCELLED = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery status for procurement
|
||||
* Backend: DeliveryStatus enum in models/enums.py
|
||||
*/
|
||||
export enum DeliveryStatus {
|
||||
PENDING = 'pending',
|
||||
IN_TRANSIT = 'in_transit',
|
||||
DELIVERED = 'delivered',
|
||||
DELAYED = 'delayed',
|
||||
CANCELLED = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority level (shared enum)
|
||||
* Backend: PriorityLevel enum in models/enums.py
|
||||
*/
|
||||
export enum PriorityLevel {
|
||||
HIGH = 'high',
|
||||
NORMAL = 'normal',
|
||||
LOW = 'low'
|
||||
}
|
||||
|
||||
/**
|
||||
* Business model (shared enum)
|
||||
* Backend: BusinessModel enum in models/enums.py
|
||||
*/
|
||||
export enum BusinessModel {
|
||||
INDIVIDUAL_BAKERY = 'individual_bakery',
|
||||
CENTRAL_BAKERY = 'central_bakery'
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PROCUREMENT REQUIREMENT TYPES
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Base procurement requirement
|
||||
* Backend: ProcurementRequirementBase schema
|
||||
*/
|
||||
export interface ProcurementRequirementBase {
|
||||
product_id: string;
|
||||
product_name: string;
|
||||
product_sku?: string;
|
||||
product_category?: string;
|
||||
product_type: string;
|
||||
required_quantity: number;
|
||||
unit_of_measure: string;
|
||||
safety_stock_quantity: number;
|
||||
total_quantity_needed: number;
|
||||
current_stock_level: number;
|
||||
reserved_stock: number;
|
||||
available_stock: number;
|
||||
net_requirement: number;
|
||||
order_demand: number;
|
||||
production_demand: number;
|
||||
forecast_demand: number;
|
||||
buffer_demand: number;
|
||||
required_by_date: string;
|
||||
lead_time_buffer_days: number;
|
||||
suggested_order_date: string;
|
||||
latest_order_date: string;
|
||||
priority: PriorityLevel;
|
||||
risk_level: RiskLevel;
|
||||
preferred_supplier_id?: string;
|
||||
backup_supplier_id?: string;
|
||||
supplier_name?: string;
|
||||
supplier_lead_time_days?: number;
|
||||
minimum_order_quantity?: number;
|
||||
estimated_unit_cost?: number;
|
||||
estimated_total_cost?: number;
|
||||
last_purchase_cost?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create procurement requirement request
|
||||
* Backend: ProcurementRequirementCreate schema
|
||||
*/
|
||||
export interface ProcurementRequirementCreate extends ProcurementRequirementBase {
|
||||
special_requirements?: string;
|
||||
storage_requirements?: string;
|
||||
shelf_life_days?: number;
|
||||
quality_specifications?: Record<string, any>;
|
||||
procurement_notes?: string;
|
||||
|
||||
// Smart procurement calculation metadata
|
||||
calculation_method?: string;
|
||||
ai_suggested_quantity?: number;
|
||||
adjusted_quantity?: number;
|
||||
adjustment_reason?: string;
|
||||
price_tier_applied?: Record<string, any>;
|
||||
supplier_minimum_applied?: boolean;
|
||||
storage_limit_applied?: boolean;
|
||||
reorder_rule_applied?: boolean;
|
||||
|
||||
// Local production support fields
|
||||
is_locally_produced?: boolean;
|
||||
recipe_id?: string;
|
||||
parent_requirement_id?: string;
|
||||
bom_explosion_level?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update procurement requirement request
|
||||
* Backend: ProcurementRequirementUpdate schema
|
||||
*/
|
||||
export interface ProcurementRequirementUpdate {
|
||||
status?: RequirementStatus;
|
||||
priority?: PriorityLevel;
|
||||
approved_quantity?: number;
|
||||
approved_cost?: number;
|
||||
purchase_order_id?: string;
|
||||
purchase_order_number?: string;
|
||||
ordered_quantity?: number;
|
||||
expected_delivery_date?: string;
|
||||
actual_delivery_date?: string;
|
||||
received_quantity?: number;
|
||||
delivery_status?: DeliveryStatus;
|
||||
procurement_notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement requirement response
|
||||
* Backend: ProcurementRequirementResponse schema
|
||||
*/
|
||||
export interface ProcurementRequirementResponse extends ProcurementRequirementBase {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
requirement_number: string;
|
||||
status: RequirementStatus;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
purchase_order_id?: string;
|
||||
purchase_order_number?: string;
|
||||
ordered_quantity: number;
|
||||
ordered_at?: string;
|
||||
expected_delivery_date?: string;
|
||||
actual_delivery_date?: string;
|
||||
received_quantity: number;
|
||||
delivery_status: DeliveryStatus;
|
||||
fulfillment_rate?: number;
|
||||
on_time_delivery?: boolean;
|
||||
quality_rating?: number;
|
||||
approved_quantity?: number;
|
||||
approved_cost?: number;
|
||||
approved_at?: string;
|
||||
approved_by?: string;
|
||||
special_requirements?: string;
|
||||
storage_requirements?: string;
|
||||
shelf_life_days?: number;
|
||||
quality_specifications?: Record<string, any>;
|
||||
procurement_notes?: string;
|
||||
|
||||
// Smart procurement calculation metadata
|
||||
calculation_method?: string;
|
||||
ai_suggested_quantity?: number;
|
||||
adjusted_quantity?: number;
|
||||
adjustment_reason?: string;
|
||||
price_tier_applied?: Record<string, any>;
|
||||
supplier_minimum_applied?: boolean;
|
||||
storage_limit_applied?: boolean;
|
||||
reorder_rule_applied?: boolean;
|
||||
|
||||
// Local production support fields
|
||||
is_locally_produced?: boolean;
|
||||
recipe_id?: string;
|
||||
parent_requirement_id?: string;
|
||||
bom_explosion_level?: number;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PROCUREMENT PLAN TYPES
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Base procurement plan
|
||||
* Backend: ProcurementPlanBase schema
|
||||
*/
|
||||
export interface ProcurementPlanBase {
|
||||
plan_date: string;
|
||||
plan_period_start: string;
|
||||
plan_period_end: string;
|
||||
planning_horizon_days: number;
|
||||
plan_type: ProcurementPlanType;
|
||||
priority: PriorityLevel;
|
||||
business_model?: BusinessModel;
|
||||
procurement_strategy: ProcurementStrategy;
|
||||
safety_stock_buffer: number;
|
||||
supply_risk_level: RiskLevel;
|
||||
demand_forecast_confidence?: number;
|
||||
seasonality_adjustment: number;
|
||||
special_requirements?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create procurement plan request
|
||||
* Backend: ProcurementPlanCreate schema
|
||||
*/
|
||||
export interface ProcurementPlanCreate extends ProcurementPlanBase {
|
||||
tenant_id: string;
|
||||
requirements?: ProcurementRequirementCreate[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update procurement plan request
|
||||
* Backend: ProcurementPlanUpdate schema
|
||||
*/
|
||||
export interface ProcurementPlanUpdate {
|
||||
status?: PlanStatus;
|
||||
priority?: PriorityLevel;
|
||||
approved_at?: string;
|
||||
approved_by?: string;
|
||||
execution_started_at?: string;
|
||||
execution_completed_at?: string;
|
||||
special_requirements?: string;
|
||||
seasonal_adjustments?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approval workflow entry
|
||||
* Backend: ApprovalWorkflowEntry (embedded in plan)
|
||||
*/
|
||||
export interface ApprovalWorkflowEntry {
|
||||
timestamp: string;
|
||||
from_status: string;
|
||||
to_status: string;
|
||||
user_id?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement plan response
|
||||
* Backend: ProcurementPlanResponse schema
|
||||
*/
|
||||
export interface ProcurementPlanResponse extends ProcurementPlanBase {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
plan_number: string;
|
||||
status: PlanStatus;
|
||||
total_requirements: number;
|
||||
total_estimated_cost: number;
|
||||
total_approved_cost: number;
|
||||
cost_variance: number;
|
||||
total_demand_orders: number;
|
||||
total_demand_quantity: number;
|
||||
total_production_requirements: number;
|
||||
primary_suppliers_count: number;
|
||||
backup_suppliers_count: number;
|
||||
supplier_diversification_score?: number;
|
||||
approved_at?: string;
|
||||
approved_by?: string;
|
||||
execution_started_at?: string;
|
||||
execution_completed_at?: string;
|
||||
fulfillment_rate?: number;
|
||||
on_time_delivery_rate?: number;
|
||||
cost_accuracy?: number;
|
||||
quality_score?: number;
|
||||
approval_workflow?: ApprovalWorkflowEntry[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
requirements: ProcurementRequirementResponse[];
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// ANALYTICS & DASHBOARD TYPES
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Procurement summary metrics
|
||||
* Backend: Returned by analytics endpoints
|
||||
*/
|
||||
export interface ProcurementSummary {
|
||||
total_plans: number;
|
||||
active_plans: number;
|
||||
total_requirements: number;
|
||||
pending_requirements: number;
|
||||
critical_requirements: number;
|
||||
total_estimated_cost: number;
|
||||
total_approved_cost: number;
|
||||
cost_variance: number;
|
||||
average_fulfillment_rate?: number;
|
||||
average_on_time_delivery?: number;
|
||||
top_suppliers: Record<string, any>[];
|
||||
critical_items: Record<string, any>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement dashboard data
|
||||
* Backend: GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement
|
||||
*/
|
||||
export interface ProcurementDashboardData {
|
||||
current_plan?: ProcurementPlanResponse;
|
||||
summary: {
|
||||
total_plans: number;
|
||||
total_estimated_cost: number;
|
||||
total_approved_cost: number;
|
||||
cost_variance: number;
|
||||
};
|
||||
upcoming_deliveries?: Record<string, any>[];
|
||||
overdue_requirements?: Record<string, any>[];
|
||||
low_stock_alerts?: Record<string, any>[];
|
||||
performance_metrics: {
|
||||
average_fulfillment_rate: number;
|
||||
average_on_time_delivery: number;
|
||||
cost_accuracy: number;
|
||||
supplier_performance: number;
|
||||
fulfillment_trend?: number;
|
||||
on_time_trend?: number;
|
||||
cost_variance_trend?: number;
|
||||
};
|
||||
|
||||
plan_status_distribution?: Array<{
|
||||
status: string;
|
||||
count: number;
|
||||
}>;
|
||||
critical_requirements?: {
|
||||
low_stock: number;
|
||||
overdue: number;
|
||||
high_priority: number;
|
||||
};
|
||||
recent_plans?: Array<{
|
||||
id: string;
|
||||
plan_number: string;
|
||||
plan_date: string;
|
||||
status: string;
|
||||
total_requirements: number;
|
||||
total_estimated_cost: number;
|
||||
created_at: string;
|
||||
}>;
|
||||
supplier_performance?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
total_orders: number;
|
||||
fulfillment_rate: number;
|
||||
on_time_rate: number;
|
||||
quality_score: number;
|
||||
}>;
|
||||
cost_by_category?: Array<{
|
||||
name: string;
|
||||
amount: number;
|
||||
}>;
|
||||
quality_metrics?: {
|
||||
avg_score: number;
|
||||
high_quality_count: number;
|
||||
low_quality_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Procurement trends data for time-series charts
|
||||
* Backend: GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement/trends
|
||||
*/
|
||||
export interface ProcurementTrendsData {
|
||||
performance_trend: Array<{
|
||||
date: string;
|
||||
fulfillment_rate: number;
|
||||
on_time_rate: number;
|
||||
}>;
|
||||
quality_trend: Array<{
|
||||
date: string;
|
||||
quality_score: number;
|
||||
}>;
|
||||
period_days: number;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// REQUEST & RESPONSE TYPES
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Generate procurement plan request
|
||||
* Backend: POST /api/v1/tenants/{tenant_id}/procurement/plans/generate
|
||||
*/
|
||||
export interface GeneratePlanRequest {
|
||||
plan_date?: string;
|
||||
force_regenerate: boolean;
|
||||
planning_horizon_days: number;
|
||||
include_safety_stock: boolean;
|
||||
safety_stock_percentage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate procurement plan response
|
||||
* Backend: POST /api/v1/tenants/{tenant_id}/procurement/plans/generate
|
||||
*/
|
||||
export interface GeneratePlanResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
plan?: ProcurementPlanResponse;
|
||||
warnings: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate procurement request (Orchestrator integration)
|
||||
* Backend: POST /api/v1/tenants/{tenant_id}/procurement/auto-generate
|
||||
*/
|
||||
export interface AutoGenerateProcurementRequest {
|
||||
forecast_data: Record<string, any>;
|
||||
production_schedule_id?: string;
|
||||
target_date?: string;
|
||||
planning_horizon_days: number;
|
||||
safety_stock_percentage: number;
|
||||
auto_create_pos: boolean;
|
||||
auto_approve_pos: boolean;
|
||||
|
||||
// Cached data from Orchestrator
|
||||
inventory_data?: Record<string, any>;
|
||||
suppliers_data?: Record<string, any>;
|
||||
recipes_data?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate procurement response
|
||||
* Backend: POST /api/v1/tenants/{tenant_id}/procurement/auto-generate
|
||||
*/
|
||||
export interface AutoGenerateProcurementResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
plan_id?: string;
|
||||
plan_number?: string;
|
||||
requirements_created: number;
|
||||
purchase_orders_created: number;
|
||||
purchase_orders_auto_approved: number;
|
||||
total_estimated_cost: number;
|
||||
warnings: string[];
|
||||
errors: string[];
|
||||
created_pos: Array<{
|
||||
po_id: string;
|
||||
po_number: string;
|
||||
supplier_id: string;
|
||||
items_count: number;
|
||||
total_amount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create purchase orders from plan result
|
||||
* Backend: POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/create-purchase-orders
|
||||
*/
|
||||
export interface CreatePOsResult {
|
||||
success: boolean;
|
||||
created_pos: {
|
||||
po_id: string;
|
||||
po_number: string;
|
||||
supplier_id: string;
|
||||
items_count: number;
|
||||
total_amount: number;
|
||||
}[];
|
||||
failed_pos: {
|
||||
supplier_id: string;
|
||||
error: string;
|
||||
}[];
|
||||
total_created: number;
|
||||
total_failed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link requirement to purchase order request
|
||||
* Backend: Used in requirement linking operations
|
||||
*/
|
||||
export interface LinkRequirementToPORequest {
|
||||
purchase_order_id: string;
|
||||
purchase_order_number: string;
|
||||
ordered_quantity: number;
|
||||
expected_delivery_date?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update delivery status request
|
||||
* Backend: Used in delivery status updates
|
||||
*/
|
||||
export interface UpdateDeliveryStatusRequest {
|
||||
delivery_status: string;
|
||||
received_quantity?: number;
|
||||
actual_delivery_date?: string;
|
||||
quality_rating?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approval request
|
||||
* Backend: Used in plan approval operations
|
||||
*/
|
||||
export interface ApprovalRequest {
|
||||
approval_notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejection request
|
||||
* Backend: Used in plan rejection operations
|
||||
*/
|
||||
export interface RejectionRequest {
|
||||
rejection_notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated procurement plans
|
||||
* Backend: GET /api/v1/tenants/{tenant_id}/procurement/plans
|
||||
*/
|
||||
export interface PaginatedProcurementPlans {
|
||||
plans: ProcurementPlanResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forecast request
|
||||
* Backend: Used in forecasting operations
|
||||
*/
|
||||
export interface ForecastRequest {
|
||||
target_date: string;
|
||||
horizon_days: number;
|
||||
include_confidence_intervals: boolean;
|
||||
product_ids?: string[];
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// QUERY PARAMETER TYPES
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Get procurement plans query parameters
|
||||
* Backend: GET /api/v1/tenants/{tenant_id}/procurement/plans
|
||||
*/
|
||||
export interface GetProcurementPlansParams {
|
||||
tenant_id: string;
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan requirements query parameters
|
||||
* Backend: GET /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/requirements
|
||||
*/
|
||||
export interface GetPlanRequirementsParams {
|
||||
tenant_id: string;
|
||||
plan_id: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update plan status parameters
|
||||
* Backend: PATCH /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/status
|
||||
*/
|
||||
export interface UpdatePlanStatusParams {
|
||||
tenant_id: string;
|
||||
plan_id: string;
|
||||
status: PlanStatus;
|
||||
notes?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user