Improve the frontend 3
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
import { equipmentService } from '../services/equipment';
|
||||
import type { Equipment, EquipmentDeletionSummary } from '../types/equipment';
|
||||
|
||||
@@ -74,11 +74,11 @@ export function useCreateEquipment(tenantId: string) {
|
||||
newEquipment
|
||||
);
|
||||
|
||||
toast.success('Equipment created successfully');
|
||||
showToast.success('Equipment created successfully');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error creating equipment:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error creating equipment');
|
||||
showToast.error(error.response?.data?.detail || 'Error creating equipment');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -104,11 +104,11 @@ export function useUpdateEquipment(tenantId: string) {
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: equipmentKeys.lists() });
|
||||
|
||||
toast.success('Equipment updated successfully');
|
||||
showToast.success('Equipment updated successfully');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error updating equipment:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error updating equipment');
|
||||
showToast.error(error.response?.data?.detail || 'Error updating equipment');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -131,11 +131,11 @@ export function useDeleteEquipment(tenantId: string) {
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: equipmentKeys.lists() });
|
||||
|
||||
toast.success('Equipment deleted successfully');
|
||||
showToast.success('Equipment deleted successfully');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error deleting equipment:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error deleting equipment');
|
||||
showToast.error(error.response?.data?.detail || 'Error deleting equipment');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -158,11 +158,11 @@ export function useHardDeleteEquipment(tenantId: string) {
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: equipmentKeys.lists() });
|
||||
|
||||
toast.success('Equipment permanently deleted');
|
||||
showToast.success('Equipment permanently deleted');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error hard deleting equipment:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error permanently deleting equipment');
|
||||
showToast.error(error.response?.data?.detail || 'Error permanently deleting equipment');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
28
frontend/src/api/hooks/orchestrator.ts
Normal file
28
frontend/src/api/hooks/orchestrator.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Orchestrator React Query hooks
|
||||
*/
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as orchestratorService from '../services/orchestrator';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Mutations
|
||||
export const useRunDailyWorkflow = (
|
||||
options?: Parameters<typeof useMutation>[0]
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (tenantId: string) =>
|
||||
orchestratorService.runDailyWorkflow(tenantId),
|
||||
onSuccess: (_, tenantId) => {
|
||||
// Invalidate queries to refresh dashboard data after workflow execution
|
||||
queryClient.invalidateQueries({ queryKey: ['procurement', 'plans'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['production', 'batches'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['forecasts'] });
|
||||
// Also invalidate dashboard queries to refresh stats
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard', 'stats'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
import { qualityTemplateService } from '../services/qualityTemplates';
|
||||
import type {
|
||||
QualityCheckTemplate,
|
||||
@@ -114,11 +114,11 @@ export function useCreateQualityTemplate(tenantId: string) {
|
||||
newTemplate
|
||||
);
|
||||
|
||||
toast.success('Plantilla de calidad creada exitosamente');
|
||||
showToast.success('Plantilla de calidad creada exitosamente');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error creating quality template:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error al crear la plantilla de calidad');
|
||||
showToast.error(error.response?.data?.detail || 'Error al crear la plantilla de calidad');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -144,11 +144,11 @@ export function useUpdateQualityTemplate(tenantId: string) {
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() });
|
||||
|
||||
toast.success('Plantilla de calidad actualizada exitosamente');
|
||||
showToast.success('Plantilla de calidad actualizada exitosamente');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error updating quality template:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error al actualizar la plantilla de calidad');
|
||||
showToast.error(error.response?.data?.detail || 'Error al actualizar la plantilla de calidad');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -171,11 +171,11 @@ export function useDeleteQualityTemplate(tenantId: string) {
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() });
|
||||
|
||||
toast.success('Plantilla de calidad eliminada exitosamente');
|
||||
showToast.success('Plantilla de calidad eliminada exitosamente');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error deleting quality template:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error al eliminar la plantilla de calidad');
|
||||
showToast.error(error.response?.data?.detail || 'Error al eliminar la plantilla de calidad');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -199,11 +199,11 @@ export function useDuplicateQualityTemplate(tenantId: string) {
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() });
|
||||
|
||||
toast.success('Plantilla de calidad duplicada exitosamente');
|
||||
showToast.success('Plantilla de calidad duplicada exitosamente');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error duplicating quality template:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error al duplicar la plantilla de calidad');
|
||||
showToast.error(error.response?.data?.detail || 'Error al duplicar la plantilla de calidad');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -233,14 +233,14 @@ export function useExecuteQualityCheck(tenantId: string) {
|
||||
: 'Control de calidad completado con observaciones';
|
||||
|
||||
if (result.overall_pass) {
|
||||
toast.success(message);
|
||||
showToast.success(message);
|
||||
} else {
|
||||
toast.error(message);
|
||||
showToast.error(message);
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error executing quality check:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error al ejecutar el control de calidad');
|
||||
showToast.error(error.response?.data?.detail || 'Error al ejecutar el control de calidad');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { settingsApi } from '../services/settings';
|
||||
import { useToast } from '../../hooks/ui/useToast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
import type {
|
||||
TenantSettings,
|
||||
TenantSettingsUpdate,
|
||||
@@ -58,7 +58,6 @@ export const useCategorySettings = (
|
||||
*/
|
||||
export const useUpdateSettings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { addToast } = useToast();
|
||||
|
||||
return useMutation<
|
||||
TenantSettings,
|
||||
@@ -69,11 +68,11 @@ export const useUpdateSettings = () => {
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all settings queries for this tenant
|
||||
queryClient.invalidateQueries({ queryKey: settingsKeys.tenant(variables.tenantId) });
|
||||
addToast('Ajustes guardados correctamente', { type: 'success' });
|
||||
showToast.success('Ajustes guardados correctamente');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update settings:', error);
|
||||
addToast('Error al guardar los ajustes', { type: 'error' });
|
||||
showToast.error('Error al guardar los ajustes');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -83,7 +82,6 @@ export const useUpdateSettings = () => {
|
||||
*/
|
||||
export const useUpdateCategorySettings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { addToast } = useToast();
|
||||
|
||||
return useMutation<
|
||||
TenantSettings,
|
||||
@@ -99,11 +97,11 @@ export const useUpdateCategorySettings = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: settingsKeys.category(variables.tenantId, variables.category),
|
||||
});
|
||||
addToast('Ajustes de categoría guardados correctamente', { type: 'success' });
|
||||
showToast.success('Ajustes de categoría guardados correctamente');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update category settings:', error);
|
||||
addToast('Error al guardar los ajustes de categoría', { type: 'error' });
|
||||
showToast.error('Error al guardar los ajustes de categoría');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -113,7 +111,6 @@ export const useUpdateCategorySettings = () => {
|
||||
*/
|
||||
export const useResetCategory = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { addToast } = useToast();
|
||||
|
||||
return useMutation<
|
||||
CategoryResetResponse,
|
||||
@@ -128,13 +125,11 @@ export const useResetCategory = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: settingsKeys.category(variables.tenantId, variables.category),
|
||||
});
|
||||
addToast(`Categoría '${variables.category}' restablecida a valores predeterminados`, {
|
||||
type: 'success',
|
||||
});
|
||||
showToast.success(`Categoría '${variables.category}' restablecida a valores predeterminados`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to reset category:', error);
|
||||
addToast('Error al restablecer la categoría', { type: 'error' });
|
||||
showToast.error('Error al restablecer la categoría');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export const useSubscription = () => {
|
||||
const currentTenant = useCurrentTenant();
|
||||
const user = useAuthUser();
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
const { notifySubscriptionChanged } = useSubscriptionEvents();
|
||||
const { notifySubscriptionChanged, subscriptionVersion } = useSubscriptionEvents();
|
||||
|
||||
// Load subscription data
|
||||
const loadSubscriptionData = useCallback(async () => {
|
||||
@@ -64,9 +64,6 @@ export const useSubscription = () => {
|
||||
features: usageSummary.usage || {},
|
||||
loading: false,
|
||||
});
|
||||
|
||||
// Notify subscribers that subscription data has changed
|
||||
notifySubscriptionChanged();
|
||||
} catch (error) {
|
||||
console.error('Error loading subscription data:', error);
|
||||
setSubscriptionInfo(prev => ({
|
||||
@@ -79,7 +76,7 @@ export const useSubscription = () => {
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscriptionData();
|
||||
}, [loadSubscriptionData]);
|
||||
}, [loadSubscriptionData, subscriptionVersion]);
|
||||
|
||||
// Check if user has a specific feature
|
||||
const hasFeature = useCallback(async (featureName: string): Promise<SubscriptionFeature> => {
|
||||
|
||||
@@ -26,6 +26,10 @@ export { productionService } from './services/production';
|
||||
export { posService } from './services/pos';
|
||||
export { recipesService } from './services/recipes';
|
||||
|
||||
// NEW: Sprint 2 & 3 Services
|
||||
export * as procurementService from './services/procurement';
|
||||
export * as orchestratorService from './services/orchestrator';
|
||||
|
||||
// Types - Auth
|
||||
export type {
|
||||
User,
|
||||
@@ -701,4 +705,9 @@ export {
|
||||
recipesKeys,
|
||||
} from './hooks/recipes';
|
||||
|
||||
// Note: All query key factories are already exported in their respective hook sections above
|
||||
// Hooks - Orchestrator
|
||||
export {
|
||||
useRunDailyWorkflow,
|
||||
} from './hooks/orchestrator';
|
||||
|
||||
// Note: All query key factories are already exported in their respective hook sections above
|
||||
|
||||
341
frontend/src/api/services/orchestrator.ts
Normal file
341
frontend/src/api/services/orchestrator.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* Orchestrator Service API Client
|
||||
* Handles coordinated workflows across Forecasting, Production, and Procurement services
|
||||
*
|
||||
* NEW in Sprint 2: Orchestrator Service coordinates the daily workflow:
|
||||
* 1. Forecasting Service → Get demand forecasts
|
||||
* 2. Production Service → Generate production schedule from forecast
|
||||
* 3. Procurement Service → Generate procurement plan from forecast + schedule
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR WORKFLOW TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface OrchestratorWorkflowRequest {
|
||||
target_date?: string; // YYYY-MM-DD, defaults to tomorrow
|
||||
planning_horizon_days?: number; // Default: 14
|
||||
|
||||
// Forecasting options
|
||||
forecast_days_ahead?: number; // Default: 7
|
||||
|
||||
// Production options
|
||||
auto_schedule_production?: boolean; // Default: true
|
||||
production_planning_days?: number; // Default: 1
|
||||
|
||||
// Procurement options
|
||||
auto_create_purchase_orders?: boolean; // Default: true
|
||||
auto_approve_purchase_orders?: boolean; // Default: false
|
||||
safety_stock_percentage?: number; // Default: 20.00
|
||||
|
||||
// Orchestrator options
|
||||
skip_on_error?: boolean; // Continue to next step if one fails
|
||||
notify_on_completion?: boolean; // Send notification when done
|
||||
}
|
||||
|
||||
export interface WorkflowStepResult {
|
||||
step: 'forecasting' | 'production' | 'procurement';
|
||||
status: 'success' | 'failed' | 'skipped';
|
||||
duration_ms: number;
|
||||
data?: any;
|
||||
error?: string;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface OrchestratorWorkflowResponse {
|
||||
success: boolean;
|
||||
workflow_id: string;
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
execution_date: string;
|
||||
total_duration_ms: number;
|
||||
|
||||
steps: WorkflowStepResult[];
|
||||
|
||||
// Step-specific results
|
||||
forecast_result?: {
|
||||
forecast_id: string;
|
||||
total_forecasts: number;
|
||||
forecast_data: any;
|
||||
};
|
||||
|
||||
production_result?: {
|
||||
schedule_id: string;
|
||||
total_batches: number;
|
||||
total_quantity: number;
|
||||
};
|
||||
|
||||
procurement_result?: {
|
||||
plan_id: string;
|
||||
total_requirements: number;
|
||||
total_cost: string;
|
||||
purchase_orders_created: number;
|
||||
purchase_orders_auto_approved: number;
|
||||
};
|
||||
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionSummary {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
target_date: string;
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
started_at: string;
|
||||
completed_at?: string;
|
||||
total_duration_ms?: number;
|
||||
steps_completed: number;
|
||||
steps_total: number;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionDetail extends WorkflowExecutionSummary {
|
||||
steps: WorkflowStepResult[];
|
||||
forecast_id?: string;
|
||||
production_schedule_id?: string;
|
||||
procurement_plan_id?: string;
|
||||
warnings?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR WORKFLOW API FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Run the daily orchestrated workflow
|
||||
* This is the main entry point for coordinated planning
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Forecasting Service: Get demand forecasts for target date
|
||||
* 2. Production Service: Generate production schedule from forecast
|
||||
* 3. Procurement Service: Generate procurement plan from forecast + schedule
|
||||
*
|
||||
* NEW in Sprint 2: Replaces autonomous schedulers with centralized orchestration
|
||||
*/
|
||||
export async function runDailyWorkflow(
|
||||
tenantId: string,
|
||||
request?: OrchestratorWorkflowRequest
|
||||
): Promise<OrchestratorWorkflowResponse> {
|
||||
return apiClient.post<OrchestratorWorkflowResponse>(
|
||||
`/tenants/${tenantId}/orchestrator/run-daily-workflow`,
|
||||
request || {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run workflow for a specific date
|
||||
*/
|
||||
export async function runWorkflowForDate(
|
||||
tenantId: string,
|
||||
targetDate: string,
|
||||
options?: Omit<OrchestratorWorkflowRequest, 'target_date'>
|
||||
): Promise<OrchestratorWorkflowResponse> {
|
||||
return runDailyWorkflow(tenantId, {
|
||||
...options,
|
||||
target_date: targetDate
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test workflow with sample data (for development/testing)
|
||||
*/
|
||||
export async function testWorkflow(
|
||||
tenantId: string
|
||||
): Promise<OrchestratorWorkflowResponse> {
|
||||
return apiClient.post<OrchestratorWorkflowResponse>(
|
||||
`/tenants/${tenantId}/orchestrator/test-workflow`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of workflow executions
|
||||
*/
|
||||
export async function listWorkflowExecutions(
|
||||
tenantId: string,
|
||||
params?: {
|
||||
status?: WorkflowExecutionSummary['status'];
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
): Promise<WorkflowExecutionSummary[]> {
|
||||
return apiClient.get<WorkflowExecutionSummary[]>(
|
||||
`/tenants/${tenantId}/orchestrator/executions`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single workflow execution by ID with full details
|
||||
*/
|
||||
export async function getWorkflowExecution(
|
||||
tenantId: string,
|
||||
executionId: string
|
||||
): Promise<WorkflowExecutionDetail> {
|
||||
return apiClient.get<WorkflowExecutionDetail>(
|
||||
`/tenants/${tenantId}/orchestrator/executions/${executionId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest workflow execution
|
||||
*/
|
||||
export async function getLatestWorkflowExecution(
|
||||
tenantId: string
|
||||
): Promise<WorkflowExecutionDetail | null> {
|
||||
const executions = await listWorkflowExecutions(tenantId, {
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (executions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getWorkflowExecution(tenantId, executions[0].id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running workflow execution
|
||||
*/
|
||||
export async function cancelWorkflowExecution(
|
||||
tenantId: string,
|
||||
executionId: string
|
||||
): Promise<{ message: string }> {
|
||||
return apiClient.post<{ message: string }>(
|
||||
`/tenants/${tenantId}/orchestrator/executions/${executionId}/cancel`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a failed workflow execution
|
||||
*/
|
||||
export async function retryWorkflowExecution(
|
||||
tenantId: string,
|
||||
executionId: string
|
||||
): Promise<OrchestratorWorkflowResponse> {
|
||||
return apiClient.post<OrchestratorWorkflowResponse>(
|
||||
`/tenants/${tenantId}/orchestrator/executions/${executionId}/retry`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR STATUS & HEALTH
|
||||
// ============================================================================
|
||||
|
||||
export interface OrchestratorStatus {
|
||||
is_leader: boolean;
|
||||
scheduler_running: boolean;
|
||||
next_scheduled_run?: string;
|
||||
last_execution?: {
|
||||
id: string;
|
||||
target_date: string;
|
||||
status: string;
|
||||
completed_at: string;
|
||||
};
|
||||
total_executions_today: number;
|
||||
total_successful_executions: number;
|
||||
total_failed_executions: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestrator service status
|
||||
*/
|
||||
export async function getOrchestratorStatus(
|
||||
tenantId: string
|
||||
): Promise<OrchestratorStatus> {
|
||||
return apiClient.get<OrchestratorStatus>(
|
||||
`/tenants/${tenantId}/orchestrator/status`
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ORCHESTRATOR CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
export interface OrchestratorConfig {
|
||||
enabled: boolean;
|
||||
schedule_cron: string; // Cron expression for daily run
|
||||
default_planning_horizon_days: number;
|
||||
auto_create_purchase_orders: boolean;
|
||||
auto_approve_purchase_orders: boolean;
|
||||
safety_stock_percentage: number;
|
||||
notify_on_completion: boolean;
|
||||
notify_on_failure: boolean;
|
||||
skip_on_error: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestrator configuration for tenant
|
||||
*/
|
||||
export async function getOrchestratorConfig(
|
||||
tenantId: string
|
||||
): Promise<OrchestratorConfig> {
|
||||
return apiClient.get<OrchestratorConfig>(
|
||||
`/tenants/${tenantId}/orchestrator/config`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update orchestrator configuration
|
||||
*/
|
||||
export async function updateOrchestratorConfig(
|
||||
tenantId: string,
|
||||
config: Partial<OrchestratorConfig>
|
||||
): Promise<OrchestratorConfig> {
|
||||
return apiClient.put<OrchestratorConfig>(
|
||||
`/tenants/${tenantId}/orchestrator/config`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Format workflow duration for display
|
||||
*/
|
||||
export function formatWorkflowDuration(durationMs: number): string {
|
||||
if (durationMs < 1000) {
|
||||
return `${durationMs}ms`;
|
||||
} else if (durationMs < 60000) {
|
||||
return `${(durationMs / 1000).toFixed(1)}s`;
|
||||
} else {
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = Math.floor((durationMs % 60000) / 1000);
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow step status icon
|
||||
*/
|
||||
export function getWorkflowStepStatusIcon(status: WorkflowStepResult['status']): string {
|
||||
switch (status) {
|
||||
case 'success': return '✅';
|
||||
case 'failed': return '❌';
|
||||
case 'skipped': return '⏭️';
|
||||
default: return '❓';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow overall status color
|
||||
*/
|
||||
export function getWorkflowStatusColor(status: WorkflowExecutionSummary['status']): string {
|
||||
switch (status) {
|
||||
case 'completed': return 'green';
|
||||
case 'running': return 'blue';
|
||||
case 'failed': return 'red';
|
||||
case 'cancelled': return 'gray';
|
||||
default: return 'gray';
|
||||
}
|
||||
}
|
||||
317
frontend/src/api/services/procurement.ts
Normal file
317
frontend/src/api/services/procurement.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Purchase Orders API Client
|
||||
* Handles all API calls for purchase orders in the suppliers service
|
||||
* Handles all API calls for purchase orders
|
||||
*
|
||||
* UPDATED in Sprint 3: Purchase orders now managed by Procurement Service
|
||||
* Previously: Suppliers Service (/tenants/{id}/purchase-orders)
|
||||
* Now: Procurement Service (/tenants/{id}/procurement/purchase-orders)
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
@@ -126,7 +130,7 @@ export async function listPurchaseOrders(
|
||||
params?: PurchaseOrderSearchParams
|
||||
): Promise<PurchaseOrderSummary[]> {
|
||||
return apiClient.get<PurchaseOrderSummary[]>(
|
||||
`/tenants/${tenantId}/purchase-orders`,
|
||||
`/tenants/${tenantId}/procurement/purchase-orders`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
@@ -160,7 +164,7 @@ export async function getPurchaseOrder(
|
||||
poId: string
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.get<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}`
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,7 +177,7 @@ export async function updatePurchaseOrder(
|
||||
data: PurchaseOrderUpdateData
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.put<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}`,
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
@@ -187,7 +191,7 @@ export async function approvePurchaseOrder(
|
||||
notes?: string
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.post<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}/approve`,
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/approve`,
|
||||
{
|
||||
action: 'approve',
|
||||
notes: notes || 'Approved from dashboard'
|
||||
@@ -204,7 +208,7 @@ export async function rejectPurchaseOrder(
|
||||
reason: string
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.post<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}/approve`,
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/approve`,
|
||||
{
|
||||
action: 'reject',
|
||||
notes: reason
|
||||
@@ -234,6 +238,6 @@ export async function deletePurchaseOrder(
|
||||
poId: string
|
||||
): Promise<{ message: string }> {
|
||||
return apiClient.delete<{ message: string }>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}`
|
||||
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,15 @@ export class SubscriptionService {
|
||||
// NEW METHODS - Centralized Plans API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Invalidate cached plan data
|
||||
* Call this when subscription changes to ensure fresh data on next fetch
|
||||
*/
|
||||
invalidateCache(): void {
|
||||
cachedPlans = null;
|
||||
lastFetchTime = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch available subscription plans with complete metadata
|
||||
* Uses cached data if available and fresh (5 min cache)
|
||||
|
||||
@@ -85,6 +85,42 @@ export interface OrderSettings {
|
||||
delivery_tracking_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface ReplenishmentSettings {
|
||||
projection_horizon_days: number;
|
||||
service_level: number;
|
||||
buffer_days: number;
|
||||
enable_auto_replenishment: boolean;
|
||||
min_order_quantity: number;
|
||||
max_order_quantity: number;
|
||||
demand_forecast_days: number;
|
||||
}
|
||||
|
||||
export interface SafetyStockSettings {
|
||||
service_level: number;
|
||||
method: string;
|
||||
min_safety_stock: number;
|
||||
max_safety_stock: number;
|
||||
reorder_point_calculation: string;
|
||||
}
|
||||
|
||||
export interface MOQSettings {
|
||||
consolidation_window_days: number;
|
||||
allow_early_ordering: boolean;
|
||||
enable_batch_optimization: boolean;
|
||||
min_batch_size: number;
|
||||
max_batch_size: number;
|
||||
}
|
||||
|
||||
export interface SupplierSelectionSettings {
|
||||
price_weight: number;
|
||||
lead_time_weight: number;
|
||||
quality_weight: number;
|
||||
reliability_weight: number;
|
||||
diversification_threshold: number;
|
||||
max_single_percentage: number;
|
||||
enable_supplier_score_optimization: boolean;
|
||||
}
|
||||
|
||||
export interface TenantSettings {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
@@ -94,6 +130,10 @@ export interface TenantSettings {
|
||||
supplier_settings: SupplierSettings;
|
||||
pos_settings: POSSettings;
|
||||
order_settings: OrderSettings;
|
||||
replenishment_settings: ReplenishmentSettings;
|
||||
safety_stock_settings: SafetyStockSettings;
|
||||
moq_settings: MOQSettings;
|
||||
supplier_selection_settings: SupplierSelectionSettings;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -105,6 +145,10 @@ export interface TenantSettingsUpdate {
|
||||
supplier_settings?: Partial<SupplierSettings>;
|
||||
pos_settings?: Partial<POSSettings>;
|
||||
order_settings?: Partial<OrderSettings>;
|
||||
replenishment_settings?: Partial<ReplenishmentSettings>;
|
||||
safety_stock_settings?: Partial<SafetyStockSettings>;
|
||||
moq_settings?: Partial<MOQSettings>;
|
||||
supplier_selection_settings?: Partial<SupplierSelectionSettings>;
|
||||
}
|
||||
|
||||
export type SettingsCategory =
|
||||
@@ -113,7 +157,11 @@ export type SettingsCategory =
|
||||
| 'production'
|
||||
| 'supplier'
|
||||
| 'pos'
|
||||
| 'order';
|
||||
| 'order'
|
||||
| 'replenishment'
|
||||
| 'safety_stock'
|
||||
| 'moq'
|
||||
| 'supplier_selection';
|
||||
|
||||
export interface CategoryResetResponse {
|
||||
category: string;
|
||||
|
||||
Reference in New Issue
Block a user