Improve AI logic

This commit is contained in:
Urtzi Alfaro
2025-11-05 13:34:56 +01:00
parent 5c87fbcf48
commit 394ad3aea4
218 changed files with 30627 additions and 7658 deletions

View File

@@ -0,0 +1,305 @@
/**
* React Hooks for AI Insights
*
* Provides React Query hooks for AI Insights API integration.
*
* Usage:
* ```tsx
* const { data: insights, isLoading } = useAIInsights(tenantId, { priority: 'high' });
* const { data: stats } = useAIInsightStats(tenantId);
* const applyMutation = useApplyInsight();
* ```
*
* Last Updated: 2025-11-03
* Status: ✅ Complete - React Query Integration
*/
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import {
aiInsightsService,
AIInsight,
AIInsightFilters,
AIInsightListResponse,
AIInsightStatsResponse,
FeedbackRequest,
OrchestrationReadyInsightsRequest,
OrchestrationReadyInsightsResponse,
} from '../services/aiInsights';
// Query Keys
export const aiInsightsKeys = {
all: ['aiInsights'] as const,
lists: () => [...aiInsightsKeys.all, 'list'] as const,
list: (tenantId: string, filters?: AIInsightFilters) => [...aiInsightsKeys.lists(), tenantId, filters] as const,
details: () => [...aiInsightsKeys.all, 'detail'] as const,
detail: (tenantId: string, insightId: string) => [...aiInsightsKeys.details(), tenantId, insightId] as const,
stats: (tenantId: string, filters?: any) => [...aiInsightsKeys.all, 'stats', tenantId, filters] as const,
orchestration: (tenantId: string, targetDate: string) => [...aiInsightsKeys.all, 'orchestration', tenantId, targetDate] as const,
dashboard: (tenantId: string) => [...aiInsightsKeys.all, 'dashboard', tenantId] as const,
};
/**
* Hook to get AI insights with filters
*/
export function useAIInsights(
tenantId: string,
filters?: AIInsightFilters,
options?: Omit<UseQueryOptions<AIInsightListResponse>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: aiInsightsKeys.list(tenantId, filters),
queryFn: () => aiInsightsService.getInsights(tenantId, filters),
staleTime: 1000 * 60 * 2, // 2 minutes
...options,
});
}
/**
* Hook to get a single AI insight
*/
export function useAIInsight(
tenantId: string,
insightId: string,
options?: Omit<UseQueryOptions<AIInsight>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: aiInsightsKeys.detail(tenantId, insightId),
queryFn: () => aiInsightsService.getInsight(tenantId, insightId),
enabled: !!insightId,
staleTime: 1000 * 60 * 5, // 5 minutes
...options,
});
}
/**
* Hook to get AI insight statistics
*/
export function useAIInsightStats(
tenantId: string,
filters?: { start_date?: string; end_date?: string },
options?: Omit<UseQueryOptions<AIInsightStatsResponse>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: aiInsightsKeys.stats(tenantId, filters),
queryFn: () => aiInsightsService.getInsightStats(tenantId, filters),
staleTime: 1000 * 60 * 5, // 5 minutes
...options,
});
}
/**
* Hook to get orchestration-ready insights
*/
export function useOrchestrationReadyInsights(
tenantId: string,
request: OrchestrationReadyInsightsRequest,
options?: Omit<UseQueryOptions<OrchestrationReadyInsightsResponse>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: aiInsightsKeys.orchestration(tenantId, request.target_date),
queryFn: () => aiInsightsService.getOrchestrationReadyInsights(tenantId, request),
enabled: !!request.target_date,
staleTime: 1000 * 60 * 10, // 10 minutes
...options,
});
}
/**
* Hook to get dashboard summary
*/
export function useAIInsightsDashboard(
tenantId: string,
options?: Omit<UseQueryOptions<any>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: aiInsightsKeys.dashboard(tenantId),
queryFn: () => aiInsightsService.getDashboardSummary(tenantId),
staleTime: 1000 * 60 * 2, // 2 minutes
...options,
});
}
/**
* Hook to get high priority insights
*/
export function useHighPriorityInsights(
tenantId: string,
limit: number = 10,
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: [...aiInsightsKeys.lists(), tenantId, 'highPriority', limit],
queryFn: () => aiInsightsService.getHighPriorityInsights(tenantId, limit),
staleTime: 1000 * 60 * 2, // 2 minutes
...options,
});
}
/**
* Hook to get actionable insights
*/
export function useActionableInsights(
tenantId: string,
limit: number = 20,
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: [...aiInsightsKeys.lists(), tenantId, 'actionable', limit],
queryFn: () => aiInsightsService.getActionableInsights(tenantId, limit),
staleTime: 1000 * 60 * 2, // 2 minutes
...options,
});
}
/**
* Hook to get insights by category
*/
export function useInsightsByCategory(
tenantId: string,
category: string,
limit: number = 20,
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: [...aiInsightsKeys.lists(), tenantId, 'category', category, limit],
queryFn: () => aiInsightsService.getInsightsByCategory(tenantId, category, limit),
enabled: !!category,
staleTime: 1000 * 60 * 2, // 2 minutes
...options,
});
}
/**
* Hook to search insights
*/
export function useSearchInsights(
tenantId: string,
query: string,
filters?: Partial<AIInsightFilters>,
options?: Omit<UseQueryOptions<AIInsight[]>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: [...aiInsightsKeys.lists(), tenantId, 'search', query, filters],
queryFn: () => aiInsightsService.searchInsights(tenantId, query, filters),
enabled: query.length > 0,
staleTime: 1000 * 30, // 30 seconds
...options,
});
}
/**
* Mutation hook to apply an insight
*/
export function useApplyInsight(
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string }>
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ tenantId, insightId }: { tenantId: string; insightId: string }) =>
aiInsightsService.applyInsight(tenantId, insightId),
onSuccess: (_, variables) => {
// Invalidate all insight queries for this tenant
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.dashboard(variables.tenantId) });
},
...options,
});
}
/**
* Mutation hook to dismiss an insight
*/
export function useDismissInsight(
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; reason?: string }>
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ tenantId, insightId, reason }) =>
aiInsightsService.dismissInsight(tenantId, insightId, reason),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.dashboard(variables.tenantId) });
},
...options,
});
}
/**
* Mutation hook to resolve an insight
*/
export function useResolveInsight(
options?: UseMutationOptions<AIInsight, Error, { tenantId: string; insightId: string; resolution?: string }>
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ tenantId, insightId, resolution }) =>
aiInsightsService.resolveInsight(tenantId, insightId, resolution),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.lists() });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.dashboard(variables.tenantId) });
},
...options,
});
}
/**
* Mutation hook to record feedback for an insight
*/
export function useRecordFeedback(
options?: UseMutationOptions<any, Error, { tenantId: string; insightId: string; feedback: FeedbackRequest }>
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ tenantId, insightId, feedback }) =>
aiInsightsService.recordFeedback(tenantId, insightId, feedback),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.detail(variables.tenantId, variables.insightId) });
queryClient.invalidateQueries({ queryKey: aiInsightsKeys.stats(variables.tenantId) });
},
...options,
});
}
/**
* Utility hook to manage insight selection
*/
export function useInsightSelection() {
const [selectedInsights, setSelectedInsights] = useState<string[]>([]);
const toggleInsight = (insightId: string) => {
setSelectedInsights((prev) =>
prev.includes(insightId)
? prev.filter((id) => id !== insightId)
: [...prev, insightId]
);
};
const selectAll = (insightIds: string[]) => {
setSelectedInsights(insightIds);
};
const clearSelection = () => {
setSelectedInsights([]);
};
return {
selectedInsights,
toggleInsight,
selectAll,
clearSelection,
isSelected: (insightId: string) => selectedInsights.includes(insightId),
};
}
// Import useState for utility hook
import { useState } from 'react';

View File

@@ -0,0 +1,446 @@
/**
* AI Insights Service
*
* Provides access to AI-generated insights from the AI Insights microservice.
* Replaces mock data with real API integration.
*
* Backend endpoints:
* - GET /tenants/{tenant_id}/insights
* - GET /tenants/{tenant_id}/insights/{insight_id}
* - POST /tenants/{tenant_id}/insights/feedback
* - GET /tenants/{tenant_id}/insights/stats
* - GET /tenants/{tenant_id}/insights/orchestration-ready
*
* Last Updated: 2025-11-03
* Status: ✅ Complete - Real API Integration
*/
import { apiClient } from '../client';
export interface AIInsight {
id: string;
tenant_id: string;
type: 'forecast' | 'warning' | 'opportunity' | 'positive' | 'optimization' | 'rule';
priority: 'urgent' | 'high' | 'medium' | 'low';
category: 'demand' | 'procurement' | 'inventory' | 'production' | 'sales' | 'system' | 'business';
title: string;
description: string;
impact_type: 'cost_savings' | 'waste_reduction' | 'yield_improvement' | 'revenue' | 'system_health' | 'process_improvement';
impact_value?: number;
impact_unit?: string;
confidence: number;
metrics_json: Record<string, any>;
actionable: boolean;
recommendation_actions?: Array<{
label: string;
action: string;
params: Record<string, any>;
}>;
source_service: string;
source_model: string;
detected_at: string;
resolved_at?: string;
resolved_by?: string;
status: 'active' | 'applied' | 'dismissed' | 'resolved';
feedback_count?: number;
avg_feedback_rating?: number;
created_at: string;
updated_at: string;
}
export interface AIInsightFilters {
type?: string;
priority?: string;
category?: string;
source_model?: string;
status?: string;
min_confidence?: number;
actionable_only?: boolean;
start_date?: string;
end_date?: string;
search?: string;
limit?: number;
offset?: number;
}
export interface AIInsightListResponse {
items: AIInsight[];
total: number;
limit: number;
offset: number;
has_more: boolean;
}
export interface AIInsightStatsResponse {
total_insights: number;
insights_by_type: Record<string, number>;
insights_by_priority: Record<string, number>;
insights_by_category: Record<string, number>;
insights_by_status: Record<string, number>;
avg_confidence: number;
total_impact_value: number;
actionable_insights: number;
resolved_insights: number;
}
export interface FeedbackRequest {
applied: boolean;
applied_at?: string;
outcome_date?: string;
outcome_metrics?: Record<string, any>;
user_rating?: number;
user_comment?: string;
}
export interface FeedbackResponse {
insight_id: string;
feedback_recorded: boolean;
feedback_id: string;
recorded_at: string;
}
export interface OrchestrationReadyInsightsRequest {
target_date: string;
min_confidence?: number;
}
export interface OrchestrationReadyInsightsResponse {
target_date: string;
insights: AIInsight[];
categorized_insights: {
demand_forecasts: AIInsight[];
supplier_alerts: AIInsight[];
inventory_optimizations: AIInsight[];
price_opportunities: AIInsight[];
yield_predictions: AIInsight[];
business_rules: AIInsight[];
other: AIInsight[];
};
total_insights: number;
}
export class AIInsightsService {
private readonly baseUrl = '/tenants';
/**
* Get all AI insights for a tenant with optional filters
*/
async getInsights(
tenantId: string,
filters?: AIInsightFilters
): Promise<AIInsightListResponse> {
const queryParams = new URLSearchParams();
if (filters?.type) queryParams.append('type', filters.type);
if (filters?.priority) queryParams.append('priority', filters.priority);
if (filters?.category) queryParams.append('category', filters.category);
if (filters?.source_model) queryParams.append('source_model', filters.source_model);
if (filters?.status) queryParams.append('status', filters.status);
if (filters?.min_confidence) queryParams.append('min_confidence', filters.min_confidence.toString());
if (filters?.actionable_only) queryParams.append('actionable_only', 'true');
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
if (filters?.search) queryParams.append('search', filters.search);
if (filters?.limit) queryParams.append('limit', filters.limit.toString());
if (filters?.offset) queryParams.append('offset', filters.offset.toString());
const url = `${this.baseUrl}/${tenantId}/insights${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiClient.get<AIInsightListResponse>(url);
}
/**
* Get a single insight by ID
*/
async getInsight(
tenantId: string,
insightId: string
): Promise<AIInsight> {
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}`;
return apiClient.get<AIInsight>(url);
}
/**
* Get insight statistics
*/
async getInsightStats(
tenantId: string,
filters?: {
start_date?: string;
end_date?: string;
}
): Promise<AIInsightStatsResponse> {
const queryParams = new URLSearchParams();
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
const url = `${this.baseUrl}/${tenantId}/insights/metrics/summary${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiClient.get<AIInsightStatsResponse>(url);
}
/**
* Get orchestration-ready insights for a specific date
*/
async getOrchestrationReadyInsights(
tenantId: string,
request: OrchestrationReadyInsightsRequest
): Promise<OrchestrationReadyInsightsResponse> {
const url = `${this.baseUrl}/${tenantId}/insights/orchestration-ready`;
const queryParams = new URLSearchParams();
queryParams.append('target_date', request.target_date);
if (request.min_confidence) {
queryParams.append('min_confidence', request.min_confidence.toString());
}
return apiClient.get<OrchestrationReadyInsightsResponse>(
`${url}?${queryParams.toString()}`
);
}
/**
* Record feedback for an applied insight
*/
async recordFeedback(
tenantId: string,
insightId: string,
feedback: FeedbackRequest
): Promise<FeedbackResponse> {
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/feedback`;
return apiClient.post<FeedbackResponse>(url, feedback);
}
/**
* Apply an insight (mark as applied)
*/
async applyInsight(
tenantId: string,
insightId: string
): Promise<AIInsight> {
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/apply`;
return apiClient.post<AIInsight>(url, {
applied_at: new Date().toISOString(),
});
}
/**
* Dismiss an insight
*/
async dismissInsight(
tenantId: string,
insightId: string,
reason?: string
): Promise<AIInsight> {
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/dismiss`;
return apiClient.post<AIInsight>(url, { reason });
}
/**
* Resolve an insight
*/
async resolveInsight(
tenantId: string,
insightId: string,
resolution?: string
): Promise<AIInsight> {
const url = `${this.baseUrl}/${tenantId}/insights/${insightId}/resolve`;
return apiClient.post<AIInsight>(url, { resolution });
}
/**
* Get insights by priority (for dashboard widgets)
*/
async getHighPriorityInsights(
tenantId: string,
limit: number = 10
): Promise<AIInsight[]> {
const response = await this.getInsights(tenantId, {
priority: 'urgent',
status: 'active',
limit,
});
if (response.items.length < limit) {
// Add high priority if not enough urgent
const highPriorityResponse = await this.getInsights(tenantId, {
priority: 'high',
status: 'active',
limit: limit - response.items.length,
});
return [...response.items, ...highPriorityResponse.items];
}
return response.items;
}
/**
* Get actionable insights (for recommendations panel)
*/
async getActionableInsights(
tenantId: string,
limit: number = 20
): Promise<AIInsight[]> {
const response = await this.getInsights(tenantId, {
actionable_only: true,
status: 'active',
limit,
});
return response.items;
}
/**
* Get insights by category
*/
async getInsightsByCategory(
tenantId: string,
category: string,
limit: number = 20
): Promise<AIInsight[]> {
const response = await this.getInsights(tenantId, {
category,
status: 'active',
limit,
});
return response.items;
}
/**
* Search insights
*/
async searchInsights(
tenantId: string,
query: string,
filters?: Partial<AIInsightFilters>
): Promise<AIInsight[]> {
const response = await this.getInsights(tenantId, {
...filters,
search: query,
limit: filters?.limit || 50,
});
return response.items;
}
/**
* Get recent insights (for activity feed)
*/
async getRecentInsights(
tenantId: string,
days: number = 7,
limit: number = 50
): Promise<AIInsight[]> {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const response = await this.getInsights(tenantId, {
start_date: startDate.toISOString(),
end_date: endDate.toISOString(),
limit,
});
return response.items;
}
/**
* Get insights summary for dashboard
*/
async getDashboardSummary(
tenantId: string
): Promise<{
stats: AIInsightStatsResponse;
highPriority: AIInsight[];
recent: AIInsight[];
}> {
const [stats, highPriority, recent] = await Promise.all([
this.getInsightStats(tenantId),
this.getHighPriorityInsights(tenantId, 5),
this.getRecentInsights(tenantId, 7, 10),
]);
return {
stats,
highPriority,
recent,
};
}
/**
* Format impact value for display
*/
formatImpactValue(insight: AIInsight): string {
if (!insight.impact_value) return 'N/A';
const value = insight.impact_value;
const unit = insight.impact_unit || 'units';
if (unit === 'euros_per_year' || unit === 'eur') {
return `${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}/year`;
} else if (unit === 'euros') {
return `${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
} else if (unit === 'percentage' || unit === 'percentage_points') {
return `${value.toFixed(1)}%`;
} else if (unit === 'units') {
return `${value.toFixed(0)} units`;
} else {
return `${value.toFixed(2)} ${unit}`;
}
}
/**
* Get priority badge color
*/
getPriorityColor(priority: string): string {
switch (priority) {
case 'urgent':
return 'red';
case 'high':
return 'orange';
case 'medium':
return 'yellow';
case 'low':
return 'blue';
default:
return 'gray';
}
}
/**
* Get type icon
*/
getTypeIcon(type: string): string {
switch (type) {
case 'forecast':
return '📈';
case 'warning':
return '⚠️';
case 'opportunity':
return '💡';
case 'positive':
return '✅';
case 'optimization':
return '🎯';
case 'rule':
return '📋';
default:
return '📊';
}
}
/**
* Calculate confidence color
*/
getConfidenceColor(confidence: number): string {
if (confidence >= 90) return 'green';
if (confidence >= 75) return 'blue';
if (confidence >= 60) return 'yellow';
return 'red';
}
}
// Export singleton instance
export const aiInsightsService = new AIInsightsService();

View File

@@ -82,7 +82,7 @@ export class ProcurementService {
/**
* Auto-generate procurement plan from forecast data (Orchestrator integration)
* POST /api/v1/tenants/{tenant_id}/procurement/auto-generate
* POST /api/v1/tenants/{tenant_id}/procurement/operations/auto-generate
*
* Called by Orchestrator Service to create procurement plans based on forecast data
*/
@@ -91,21 +91,21 @@ export class ProcurementService {
request: AutoGenerateProcurementRequest
): Promise<AutoGenerateProcurementResponse> {
return apiClient.post<AutoGenerateProcurementResponse>(
`/tenants/${tenantId}/procurement/auto-generate`,
`/tenants/${tenantId}/procurement/operations/auto-generate`,
request
);
}
/**
* Generate a new procurement plan (manual/UI-driven)
* POST /api/v1/tenants/{tenant_id}/procurement/plans/generate
* POST /api/v1/tenants/{tenant_id}/procurement/plans
*/
static async generateProcurementPlan(
tenantId: string,
request: GeneratePlanRequest
): Promise<GeneratePlanResponse> {
return apiClient.post<GeneratePlanResponse>(
`/tenants/${tenantId}/procurement/plans/generate`,
`/tenants/${tenantId}/procurement/plans`,
request
);
}
@@ -330,6 +330,121 @@ export class ProcurementService {
{ auto_approve: autoApprove }
);
}
/**
* Create a new purchase order
* POST /api/v1/tenants/{tenant_id}/procurement/purchase-orders
*/
static async createPurchaseOrder(
tenantId: string,
poData: any
): Promise<PurchaseOrderResponse> {
return apiClient.post<PurchaseOrderResponse>(
`/tenants/${tenantId}/procurement/purchase-orders`,
poData
);
}
/**
* Get purchase order by ID
* GET /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}
*/
static async getPurchaseOrderById(
tenantId: string,
poId: string
): Promise<PurchaseOrderWithSupplierResponse> {
return apiClient.get<PurchaseOrderWithSupplierResponse>(
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`
);
}
/**
* List purchase orders
* GET /api/v1/tenants/{tenant_id}/procurement/purchase-orders
*/
static async getPurchaseOrders(
tenantId: string,
params?: { skip?: number; limit?: number; supplier_id?: string; status?: string }
): Promise<PurchaseOrderResponse[]> {
const queryParams = new URLSearchParams();
if (params?.skip !== undefined) queryParams.append('skip', params.skip.toString());
if (params?.limit !== undefined) queryParams.append('limit', params.limit.toString());
if (params?.supplier_id) queryParams.append('supplier_id', params.supplier_id);
if (params?.status) queryParams.append('status', params.status);
const queryString = queryParams.toString();
const url = `/tenants/${tenantId}/procurement/purchase-orders${queryString ? `?${queryString}` : ''}`;
return apiClient.get<PurchaseOrderResponse[]>(url);
}
/**
* Update purchase order
* PATCH /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}
*/
static async updatePurchaseOrder(
tenantId: string,
poId: string,
poData: any
): Promise<PurchaseOrderResponse> {
return apiClient.patch<PurchaseOrderResponse>(
`/tenants/${tenantId}/procurement/purchase-orders/${poId}`,
poData
);
}
/**
* Update purchase order status
* PATCH /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}/status
*/
static async updatePurchaseOrderStatus(
tenantId: string,
poId: string,
status: string,
notes?: string
): Promise<PurchaseOrderResponse> {
const queryParams = new URLSearchParams({ status });
if (notes) queryParams.append('notes', notes);
return apiClient.patch<PurchaseOrderResponse>(
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/status?${queryParams.toString()}`,
{}
);
}
/**
* Approve or reject purchase order
* POST /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}/approve
*/
static async approvePurchaseOrder(
tenantId: string,
poId: string,
approveData: any
): Promise<PurchaseOrderResponse> {
return apiClient.post<PurchaseOrderResponse>(
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/approve`,
approveData
);
}
/**
* Cancel purchase order
* POST /api/v1/tenants/{tenant_id}/procurement/purchase-orders/{po_id}/cancel
*/
static async cancelPurchaseOrder(
tenantId: string,
poId: string,
reason: string,
cancelledBy?: string
): Promise<PurchaseOrderResponse> {
const queryParams = new URLSearchParams({ reason });
if (cancelledBy) queryParams.append('cancelled_by', cancelledBy);
return apiClient.post<PurchaseOrderResponse>(
`/tenants/${tenantId}/procurement/purchase-orders/${poId}/cancel?${queryParams.toString()}`,
{}
);
}
}
export default ProcurementService;

View File

@@ -121,6 +121,31 @@ export interface SupplierSelectionSettings {
enable_supplier_score_optimization: boolean;
}
export interface MLInsightsSettings {
// Inventory ML (Safety Stock Optimization)
inventory_lookback_days: number;
inventory_min_history_days: number;
// Production ML (Yield Prediction)
production_lookback_days: number;
production_min_history_runs: number;
// Procurement ML (Supplier Analysis & Price Forecasting)
supplier_analysis_lookback_days: number;
supplier_analysis_min_orders: number;
price_forecast_lookback_days: number;
price_forecast_horizon_days: number;
// Forecasting ML (Dynamic Rules)
rules_generation_lookback_days: number;
rules_generation_min_samples: number;
// Global ML Settings
enable_ml_insights: boolean;
ml_insights_auto_trigger: boolean;
ml_confidence_threshold: number;
}
export interface TenantSettings {
id: string;
tenant_id: string;
@@ -134,6 +159,7 @@ export interface TenantSettings {
safety_stock_settings: SafetyStockSettings;
moq_settings: MOQSettings;
supplier_selection_settings: SupplierSelectionSettings;
ml_insights_settings: MLInsightsSettings;
created_at: string;
updated_at: string;
}
@@ -149,6 +175,7 @@ export interface TenantSettingsUpdate {
safety_stock_settings?: Partial<SafetyStockSettings>;
moq_settings?: Partial<MOQSettings>;
supplier_selection_settings?: Partial<SupplierSelectionSettings>;
ml_insights_settings?: Partial<MLInsightsSettings>;
}
export type SettingsCategory =
@@ -161,7 +188,8 @@ export type SettingsCategory =
| 'replenishment'
| 'safety_stock'
| 'moq'
| 'supplier_selection';
| 'supplier_selection'
| 'ml_insights';
export interface CategoryResetResponse {
category: string;