Improve the frontend 3

This commit is contained in:
Urtzi Alfaro
2025-10-30 21:08:07 +01:00
parent 36217a2729
commit 63f5c6d512
184 changed files with 21512 additions and 7442 deletions

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

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

View File

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

View File

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