Improve the frontend 4

This commit is contained in:
Urtzi Alfaro
2025-11-01 21:35:03 +01:00
parent f44d235c6d
commit 0220da1725
59 changed files with 5785 additions and 1870 deletions

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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