diff --git a/frontend/src/api/hooks/orders.ts b/frontend/src/api/hooks/orders.ts index 4118373d..946113f9 100644 --- a/frontend/src/api/hooks/orders.ts +++ b/frontend/src/api/hooks/orders.ts @@ -18,6 +18,19 @@ import { GetCustomersParams, UpdateOrderStatusParams, GetDemandRequirementsParams, + // Procurement types + ProcurementPlanResponse, + ProcurementPlanCreate, + ProcurementPlanUpdate, + ProcurementRequirementResponse, + ProcurementRequirementUpdate, + ProcurementDashboardData, + GeneratePlanRequest, + GeneratePlanResponse, + PaginatedProcurementPlans, + GetProcurementPlansParams, + GetPlanRequirementsParams, + UpdatePlanStatusParams, } from '../types/orders'; import { ApiError } from '../client/apiClient'; @@ -42,6 +55,17 @@ 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 ===== @@ -329,4 +353,195 @@ export const useInvalidateOrders = () => { }); }, }; +}; + +// ===== Procurement Queries ===== + +export const useProcurementPlans = ( + params: GetProcurementPlansParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.currentProcurementPlan(tenantId), + queryFn: () => OrdersService.getCurrentProcurementPlan(tenantId), + staleTime: 1 * 60 * 1000, // 1 minute + enabled: !!tenantId, + ...options, + }); +}; + +export const useProcurementDashboard = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.procurementDashboard(tenantId), + queryFn: () => OrdersService.getProcurementDashboard(tenantId), + staleTime: 2 * 60 * 1000, // 2 minutes + enabled: !!tenantId, + ...options, + }); +}; + +export const usePlanRequirements = ( + params: GetPlanRequirementsParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.criticalRequirements(tenantId), + queryFn: () => OrdersService.getCriticalRequirements(tenantId), + staleTime: 1 * 60 * 1000, // 1 minute + enabled: !!tenantId, + ...options, + }); +}; + +export const useProcurementHealth = ( + tenantId: string, + options?: Omit, '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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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, + }); }; \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 85303148..ad979ca3 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -269,6 +269,30 @@ export type { GetCustomersParams, UpdateOrderStatusParams, GetDemandRequirementsParams, + // Procurement types + ProcurementPlanType, + ProcurementStrategy, + RiskLevel, + RequirementStatus, + PlanStatus, + DeliveryStatus, + ProcurementRequirementBase, + ProcurementRequirementCreate, + ProcurementRequirementUpdate, + ProcurementRequirementResponse, + ProcurementPlanBase, + ProcurementPlanCreate, + ProcurementPlanUpdate, + ProcurementPlanResponse, + ProcurementSummary, + ProcurementDashboardData, + GeneratePlanRequest, + GeneratePlanResponse, + PaginatedProcurementPlans, + ForecastRequest, + GetProcurementPlansParams, + GetPlanRequirementsParams, + UpdatePlanStatusParams, } from './types/orders'; // Hooks - Auth @@ -512,6 +536,18 @@ export { useCreateCustomer, useUpdateCustomer, useInvalidateOrders, + // Procurement hooks + useProcurementPlans, + useProcurementPlan, + useProcurementPlanByDate, + useCurrentProcurementPlan, + useProcurementDashboard, + usePlanRequirements, + useCriticalRequirements, + useProcurementHealth, + useGenerateProcurementPlan, + useUpdateProcurementPlanStatus, + useTriggerDailyScheduler, ordersKeys, } from './hooks/orders'; diff --git a/frontend/src/api/services/orders.ts b/frontend/src/api/services/orders.ts index 5e7da533..acb97fef 100644 --- a/frontend/src/api/services/orders.ts +++ b/frontend/src/api/services/orders.ts @@ -21,6 +21,19 @@ import { GetCustomersParams, UpdateOrderStatusParams, GetDemandRequirementsParams, + // Procurement types + ProcurementPlanResponse, + ProcurementPlanCreate, + ProcurementPlanUpdate, + ProcurementRequirementResponse, + ProcurementRequirementUpdate, + ProcurementDashboardData, + GeneratePlanRequest, + GeneratePlanResponse, + PaginatedProcurementPlans, + GetProcurementPlansParams, + GetPlanRequirementsParams, + UpdatePlanStatusParams, } from '../types/orders'; export class OrdersService { @@ -168,6 +181,127 @@ export class OrdersService { static async getServiceStatus(tenantId: string): Promise { return apiClient.get(`/tenants/${tenantId}/orders/status`); } + + // ===== Procurement Planning Endpoints ===== + + /** + * Get current procurement plan for today + * GET /tenants/{tenant_id}/procurement/plans/current + */ + static async getCurrentProcurementPlan(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/plans/current`); + } + + /** + * Get procurement plan by specific date + * GET /tenants/{tenant_id}/procurement/plans/date/{plan_date} + */ + static async getProcurementPlanByDate(tenantId: string, planDate: string): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/plans/date/${planDate}`); + } + + /** + * Get procurement plan by ID + * GET /tenants/{tenant_id}/procurement/plans/id/{plan_id} + */ + static async getProcurementPlanById(tenantId: string, planId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/plans/id/${planId}`); + } + + /** + * List procurement plans with filtering + * GET /tenants/{tenant_id}/procurement/plans/ + */ + static async getProcurementPlans(params: GetProcurementPlansParams): Promise { + 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( + `/tenants/${tenant_id}/procurement/plans?${queryParams.toString()}` + ); + } + + /** + * Generate a new procurement plan + * POST /tenants/{tenant_id}/procurement/plans/generate + */ + static async generateProcurementPlan(tenantId: string, request: GeneratePlanRequest): Promise { + return apiClient.post(`/tenants/${tenantId}/procurement/plans/generate`, request); + } + + /** + * Update procurement plan status + * PUT /tenants/{tenant_id}/procurement/plans/{plan_id}/status + */ + static async updateProcurementPlanStatus(params: UpdatePlanStatusParams): Promise { + const { tenant_id, plan_id, status } = params; + + const queryParams = new URLSearchParams({ status }); + + return apiClient.put( + `/tenants/${tenant_id}/procurement/plans/${plan_id}/status?${queryParams.toString()}`, + {} + ); + } + + /** + * Get procurement dashboard data + * GET /tenants/{tenant_id}/procurement/dashboard + */ + static async getProcurementDashboard(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/dashboard`); + } + + /** + * Get requirements for a specific plan + * GET /tenants/{tenant_id}/procurement/plans/{plan_id}/requirements + */ + static async getPlanRequirements(params: GetPlanRequirementsParams): Promise { + 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(url); + } + + /** + * Get critical requirements across all plans + * GET /tenants/{tenant_id}/procurement/requirements/critical + */ + static async getCriticalRequirements(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/requirements/critical`); + } + + /** + * Trigger daily scheduler manually + * POST /tenants/{tenant_id}/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}/procurement/scheduler/trigger`, + {} + ); + } + + /** + * Get procurement service health + * GET /tenants/{tenant_id}/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}/procurement/health`); + } } export default OrdersService; \ No newline at end of file diff --git a/frontend/src/api/types/orders.ts b/frontend/src/api/types/orders.ts index 1005f403..a1774f6f 100644 --- a/frontend/src/api/types/orders.ts +++ b/frontend/src/api/types/orders.ts @@ -280,4 +280,243 @@ export interface UpdateOrderStatusParams { export interface GetDemandRequirementsParams { tenant_id: string; target_date: string; +} + +// ===== Procurement Types ===== + +export type ProcurementPlanType = 'regular' | 'emergency' | 'seasonal'; +export type ProcurementStrategy = 'just_in_time' | 'bulk' | 'mixed'; +export type RiskLevel = 'low' | 'medium' | 'high' | 'critical'; +export type RequirementStatus = 'pending' | 'approved' | 'ordered' | 'partially_received' | 'received' | 'cancelled'; +export type PlanStatus = 'draft' | 'pending_approval' | 'approved' | 'in_execution' | 'completed' | 'cancelled'; +export type DeliveryStatus = 'pending' | 'in_transit' | 'delivered' | 'delayed' | 'cancelled'; + +// 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; + 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; + procurement_notes?: string; +} + +// 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; +} + +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; + 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[]; + critical_items: Record[]; +} + +export interface ProcurementDashboardData { + current_plan?: ProcurementPlanResponse; + summary: ProcurementSummary; + upcoming_deliveries: Record[]; + overdue_requirements: Record[]; + low_stock_alerts: Record[]; + performance_metrics: Record; +} + +// 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[]; +} + +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; } \ No newline at end of file diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx index 810f3415..9f078a33 100644 --- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx +++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx @@ -1,239 +1,209 @@ import React, { useState } from 'react'; -import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react'; -import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui'; +import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader } from 'lucide-react'; +import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui'; import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; +import { + useProcurementDashboard, + useProcurementPlans, + useCurrentProcurementPlan, + useCriticalRequirements, + useGenerateProcurementPlan, + useUpdateProcurementPlanStatus, + useTriggerDailyScheduler +} from '../../../../api'; +import { useTenantStore } from '../../../../stores/tenant.store'; const ProcurementPage: React.FC = () => { - const [activeTab, setActiveTab] = useState('orders'); + const [activeTab, setActiveTab] = useState('plans'); const [searchTerm, setSearchTerm] = useState(''); const [showForm, setShowForm] = useState(false); const [modalMode, setModalMode] = useState<'view' | 'edit'>('view'); - const [selectedOrder, setSelectedOrder] = useState(null); + const [selectedPlan, setSelectedPlan] = useState(null); + + const { currentTenant } = useTenantStore(); + const tenantId = currentTenant?.id || ''; - const mockPurchaseOrders = [ - { - id: 'PO-2024-001', - supplier: 'Molinos del Sur', - status: 'pending', - orderDate: '2024-01-25', - deliveryDate: '2024-01-28', - totalAmount: 1250.00, - items: [ - { name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 }, - { name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 }, - ], - paymentStatus: 'pending', - notes: 'Entrega en horario de mañana', - }, - { - id: 'PO-2024-002', - supplier: 'Levaduras SA', - status: 'delivered', - orderDate: '2024-01-20', - deliveryDate: '2024-01-23', - totalAmount: 425.50, - items: [ - { name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 }, - { name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 }, - ], - paymentStatus: 'paid', - notes: '', - }, - { - id: 'PO-2024-003', - supplier: 'Lácteos Frescos', - status: 'in_transit', - orderDate: '2024-01-24', - deliveryDate: '2024-01-26', - totalAmount: 320.75, - items: [ - { name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 }, - { name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 }, - ], - paymentStatus: 'pending', - notes: 'Producto refrigerado', - }, - ]; + // Real API data hooks + const { data: dashboardData, isLoading: isDashboardLoading } = useProcurementDashboard(tenantId); + const { data: procurementPlans, isLoading: isPlansLoading } = useProcurementPlans({ + tenant_id: tenantId, + limit: 50, + offset: 0 + }); + const { data: currentPlan, isLoading: isCurrentPlanLoading } = useCurrentProcurementPlan(tenantId); + const { data: criticalRequirements, isLoading: isCriticalLoading } = useCriticalRequirements(tenantId); + + const generatePlanMutation = useGenerateProcurementPlan(); + const updatePlanStatusMutation = useUpdateProcurementPlanStatus(); + const triggerSchedulerMutation = useTriggerDailyScheduler(); - const mockSuppliers = [ - { - id: '1', - name: 'Molinos del Sur', - contact: 'Juan Pérez', - email: 'juan@molinosdelsur.com', - phone: '+34 91 234 5678', - category: 'Harinas', - rating: 4.8, - totalOrders: 24, - totalSpent: 15600.00, - paymentTerms: '30 días', - leadTime: '2-3 días', - location: 'Sevilla', - status: 'active', - }, - { - id: '2', - name: 'Levaduras SA', - contact: 'María González', - email: 'maria@levaduras.com', - phone: '+34 93 456 7890', - category: 'Levaduras', - rating: 4.6, - totalOrders: 18, - totalSpent: 8450.00, - paymentTerms: '15 días', - leadTime: '1-2 días', - location: 'Barcelona', - status: 'active', - }, - { - id: '3', - name: 'Lácteos Frescos', - contact: 'Carlos Ruiz', - email: 'carlos@lacteosfrescos.com', - phone: '+34 96 789 0123', - category: 'Lácteos', - rating: 4.4, - totalOrders: 32, - totalSpent: 12300.00, - paymentTerms: '20 días', - leadTime: '1 día', - location: 'Valencia', - status: 'active', - }, - ]; + if (!tenantId) { + return ( +
+
+

+ No hay tenant seleccionado +

+

+ Selecciona un tenant para ver los datos de procurement +

+
+
+ ); + } - const getPurchaseStatusConfig = (status: string, paymentStatus: string) => { + + const getPlanStatusConfig = (status: string) => { const statusConfig = { - pending: { text: 'Pendiente', icon: Clock }, + draft: { text: 'Borrador', icon: Clock }, + pending_approval: { text: 'Pendiente Aprobación', icon: Clock }, approved: { text: 'Aprobado', icon: CheckCircle }, - in_transit: { text: 'En Tránsito', icon: Truck }, - delivered: { text: 'Entregado', icon: CheckCircle }, + in_execution: { text: 'En Ejecución', icon: Truck }, + completed: { text: 'Completado', icon: CheckCircle }, cancelled: { text: 'Cancelado', icon: AlertCircle }, }; const config = statusConfig[status as keyof typeof statusConfig]; const Icon = config?.icon; - const isPaymentPending = paymentStatus === 'pending'; - const isOverdue = paymentStatus === 'overdue'; return { - color: getStatusColor(status === 'in_transit' ? 'inTransit' : status), + color: getStatusColor(status === 'in_execution' ? 'inTransit' : status === 'pending_approval' ? 'pending' : status), text: config?.text || status, icon: Icon, - isCritical: isOverdue, - isHighlight: isPaymentPending + isCritical: status === 'cancelled', + isHighlight: status === 'pending_approval' }; }; - const filteredOrders = mockPurchaseOrders.filter(order => { - const matchesSearch = order.supplier.toLowerCase().includes(searchTerm.toLowerCase()) || - order.id.toLowerCase().includes(searchTerm.toLowerCase()) || - order.notes.toLowerCase().includes(searchTerm.toLowerCase()); + const filteredPlans = procurementPlans?.plans?.filter(plan => { + const matchesSearch = plan.plan_number.toLowerCase().includes(searchTerm.toLowerCase()) || + plan.status.toLowerCase().includes(searchTerm.toLowerCase()) || + (plan.special_requirements && plan.special_requirements.toLowerCase().includes(searchTerm.toLowerCase())); return matchesSearch; - }); + }) || []; - const mockPurchaseStats = { - totalOrders: mockPurchaseOrders.length, - pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length, - inTransit: mockPurchaseOrders.filter(o => o.status === 'in_transit').length, - delivered: mockPurchaseOrders.filter(o => o.status === 'delivered').length, - totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0), - activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length, + const stats = { + totalPlans: dashboardData?.summary?.total_plans || 0, + activePlans: dashboardData?.summary?.active_plans || 0, + pendingRequirements: dashboardData?.summary?.pending_requirements || 0, + criticalRequirements: dashboardData?.summary?.critical_requirements || 0, + totalEstimatedCost: dashboardData?.summary?.total_estimated_cost || 0, + totalApprovedCost: dashboardData?.summary?.total_approved_cost || 0, }; - const purchaseOrderStats = [ + const procurementStats = [ { - title: 'Total Órdenes', - value: mockPurchaseStats.totalOrders, + title: 'Planes Totales', + value: stats.totalPlans, variant: 'default' as const, - icon: ShoppingCart, + icon: Package, }, { - title: 'Pendientes', - value: mockPurchaseStats.pendingOrders, - variant: 'warning' as const, - icon: Clock, - }, - { - title: 'En Tránsito', - value: mockPurchaseStats.inTransit, - variant: 'info' as const, - icon: Truck, - }, - { - title: 'Entregadas', - value: mockPurchaseStats.delivered, + title: 'Planes Activos', + value: stats.activePlans, variant: 'success' as const, icon: CheckCircle, }, { - title: 'Gasto Total', - value: formatters.currency(mockPurchaseStats.totalSpent), - variant: 'success' as const, + title: 'Requerimientos Pendientes', + value: stats.pendingRequirements, + variant: 'warning' as const, + icon: Clock, + }, + { + title: 'Críticos', + value: stats.criticalRequirements, + variant: 'warning' as const, + icon: AlertCircle, + }, + { + title: 'Costo Estimado', + value: formatters.currency(stats.totalEstimatedCost), + variant: 'info' as const, icon: DollarSign, }, { - title: 'Proveedores', - value: mockPurchaseStats.activeSuppliers, - variant: 'info' as const, - icon: Package, + title: 'Costo Aprobado', + value: formatters.currency(stats.totalApprovedCost), + variant: 'success' as const, + icon: DollarSign, }, ]; return (
console.log('Export purchase orders') + onClick: () => console.log('Export procurement data') }, { - id: "new", - label: "Nueva Orden de Compra", + id: "generate", + label: "Generar Plan", variant: "primary" as const, icon: Plus, - onClick: () => console.log('New purchase order') + onClick: () => generatePlanMutation.mutate({ + tenantId, + request: { + force_regenerate: false, + planning_horizon_days: 14, + include_safety_stock: true, + safety_stock_percentage: 20 + } + }) + }, + { + id: "trigger", + label: "Ejecutar Programador", + variant: "outline" as const, + icon: Calendar, + onClick: () => triggerSchedulerMutation.mutate(tenantId) } ]} /> {/* Stats Grid */} - + {isDashboardLoading ? ( +
+ +
+ ) : ( + + )} {/* Tabs Navigation */}
- {activeTab === 'orders' && ( + {activeTab === 'plans' && (
setSearchTerm(e.target.value)} className="w-full" @@ -267,148 +237,151 @@ const ProcurementPage: React.FC = () => { )} - {/* Purchase Orders Grid */} - {activeTab === 'orders' && ( + {/* Procurement Plans Grid */} + {activeTab === 'plans' && (
- {filteredOrders.map((order) => { - const statusConfig = getPurchaseStatusConfig(order.status, order.paymentStatus); - const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : order.paymentStatus === 'overdue' ? 'Pago vencido' : ''; - - return ( - { - setSelectedOrder(order); - setModalMode('view'); - setShowForm(true); - } - }, - { - label: 'Editar', - icon: Edit, - variant: 'outline', - onClick: () => { - setSelectedOrder(order); - setModalMode('edit'); - setShowForm(true); - } - } - ]} - /> - ); - })} + {isPlansLoading ? ( +
+ +
+ ) : ( + filteredPlans.map((plan) => { + const statusConfig = getPlanStatusConfig(plan.status); + + return ( + { + setSelectedPlan(plan); + setModalMode('view'); + setShowForm(true); + } + }, + ...(plan.status === 'pending_approval' ? [{ + label: 'Aprobar', + icon: CheckCircle, + variant: 'outline' as const, + onClick: () => { + updatePlanStatusMutation.mutate({ + tenant_id: tenantId, + plan_id: plan.id, + status: 'approved' + }); + } + }] : []) + ]} + /> + ); + }) + )}
)} - {/* Empty State for Purchase Orders */} - {activeTab === 'orders' && filteredOrders.length === 0 && ( + {/* Empty State for Procurement Plans */} + {activeTab === 'plans' && !isPlansLoading && filteredPlans.length === 0 && (
- +

- No se encontraron órdenes de compra + No se encontraron planes de compra

- Intenta ajustar la búsqueda o crear una nueva orden de compra + Intenta ajustar la búsqueda o generar un nuevo plan de compra

-
)} - {activeTab === 'suppliers' && ( -
- {mockSuppliers.map((supplier) => ( - -
-
-

{supplier.name}

-

{supplier.category}

-
- Activo -
- -
-
- Contacto: - {supplier.contact} -
-
- Email: - {supplier.email} -
-
- Teléfono: - {supplier.phone} -
-
- Ubicación: - {supplier.location} -
-
- -
-
-
-

Valoración

-

- - {supplier.rating} -

-
-
-

Pedidos

-

{supplier.totalOrders}

-
-
- -
-
-

Total Gastado

-

€{supplier.totalSpent.toLocaleString()}

-
-
-

Tiempo Entrega

-

{supplier.leadTime}

-
-
- -
-

Condiciones de Pago

-

{supplier.paymentTerms}

-
- -
- - -
-
-
- ))} + {activeTab === 'requirements' && ( +
+ {isCriticalLoading ? ( +
+ +
+ ) : criticalRequirements && criticalRequirements.length > 0 ? ( +
+ {criticalRequirements.map((requirement) => ( + console.log('View requirement details') + } + ]} + /> + ))} +
+ ) : ( +
+ +

+ No hay requerimientos críticos +

+

+ Todos los requerimientos están bajo control +

+
+ )}
)} @@ -416,100 +389,139 @@ const ProcurementPage: React.FC = () => {
-

Gastos por Mes

-
-

Gráfico de gastos mensuales

+

Costos de Procurement

+
+
+ Costo Estimado Total + + {formatters.currency(stats.totalEstimatedCost)} + +
+
+ Costo Aprobado + + {formatters.currency(stats.totalApprovedCost)} + +
+
+ Varianza + + {formatters.currency(stats.totalEstimatedCost - stats.totalApprovedCost)} + +
-

Top Proveedores

+

Alertas Críticas

- {mockSuppliers - .sort((a, b) => b.totalSpent - a.totalSpent) - .slice(0, 5) - .map((supplier, index) => ( -
-
- - {index + 1}. - - {supplier.name} -
- - €{supplier.totalSpent.toLocaleString()} - + {dashboardData?.low_stock_alerts?.slice(0, 5).map((alert: any, index: number) => ( +
+
+ + {alert.product_name || `Alerta ${index + 1}`}
- ))} + + Stock Bajo + +
+ )) || ( +
+ +

No hay alertas críticas

+
+ )}
-

Gastos por Categoría

-
-

Gráfico de gastos por categoría

+

Resumen de Performance

+
+
+

{stats.totalPlans}

+

Planes Totales

+
+
+

{stats.activePlans}

+

Planes Activos

+
+
+

{stats.pendingRequirements}

+

Pendientes

+
+
+

{stats.criticalRequirements}

+

Críticos

+
)} - {/* Purchase Order Modal */} - {showForm && selectedOrder && ( + {/* Procurement Plan Modal */} + {showForm && selectedPlan && ( { setShowForm(false); - setSelectedOrder(null); + setSelectedPlan(null); setModalMode('view'); }} mode={modalMode} onModeChange={setModalMode} - title={selectedOrder.supplier} - subtitle={`Orden de Compra ${selectedOrder.id}`} - statusIndicator={getPurchaseStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)} + title={`Plan de Compra ${selectedPlan.plan_number}`} + subtitle={new Date(selectedPlan.plan_date).toLocaleDateString('es-ES')} + statusIndicator={getPlanStatusConfig(selectedPlan.status)} size="lg" sections={[ { - title: 'Información del Proveedor', + title: 'Información del Plan', icon: Package, fields: [ { - label: 'Proveedor', - value: selectedOrder.supplier, - highlight: true, - editable: true, - required: true, - placeholder: 'Nombre del proveedor' + label: 'Número de Plan', + value: selectedPlan.plan_number, + highlight: true }, { - label: 'ID de Orden', - value: selectedOrder.id + label: 'Tipo de Plan', + value: selectedPlan.plan_type }, { - label: 'Estado de Pago', - value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : selectedOrder.paymentStatus === 'pending' ? 'Pendiente' : 'Vencido', + label: 'Estrategia', + value: selectedPlan.procurement_strategy + }, + { + label: 'Prioridad', + value: selectedPlan.priority, type: 'status' } ] }, { - title: 'Fechas Importantes', + title: 'Fechas y Períodos', icon: Calendar, fields: [ { - label: 'Fecha de pedido', - value: selectedOrder.orderDate, - type: 'date', - editable: true + label: 'Fecha del Plan', + value: selectedPlan.plan_date, + type: 'date' }, { - label: 'Fecha de entrega', - value: selectedOrder.deliveryDate, + label: 'Período de Inicio', + value: selectedPlan.plan_period_start, + type: 'date' + }, + { + label: 'Período de Fin', + value: selectedPlan.plan_period_end, type: 'date', - highlight: true, - editable: true, - required: true + highlight: true + }, + { + label: 'Horizonte de Planificación', + value: `${selectedPlan.planning_horizon_days} días` } ] }, @@ -518,47 +530,60 @@ const ProcurementPage: React.FC = () => { icon: DollarSign, fields: [ { - label: 'Importe total', - value: selectedOrder.totalAmount, + label: 'Costo Estimado Total', + value: selectedPlan.total_estimated_cost, type: 'currency', - highlight: true, - editable: true, - required: true, - placeholder: '0.00' + highlight: true }, { - label: 'Número de artículos', - value: `${selectedOrder.items?.length} productos` + label: 'Costo Aprobado Total', + value: selectedPlan.total_approved_cost, + type: 'currency' + }, + { + label: 'Varianza de Costo', + value: selectedPlan.cost_variance, + type: 'currency' } ] }, { - title: 'Artículos Pedidos', + title: 'Estadísticas', icon: ShoppingCart, fields: [ { - label: 'Lista de productos', - value: selectedOrder.items?.map(item => `${item.name}: ${item.quantity} ${item.unit} - ${formatters.currency(item.total)}`), - type: 'list', - span: 2 + label: 'Total de Requerimientos', + value: `${selectedPlan.total_requirements} requerimientos` + }, + { + label: 'Demanda Total (Cantidad)', + value: `${selectedPlan.total_demand_quantity} unidades` + }, + { + label: 'Proveedores Primarios', + value: `${selectedPlan.primary_suppliers_count} proveedores` + }, + { + label: 'Proveedores de Respaldo', + value: `${selectedPlan.backup_suppliers_count} proveedores` } ] }, - ...(selectedOrder.notes ? [{ - title: 'Notas Adicionales', + ...(selectedPlan.special_requirements ? [{ + title: 'Requerimientos Especiales', fields: [ { label: 'Observaciones', - value: selectedOrder.notes, + value: selectedPlan.special_requirements, span: 2 as const, editable: true, - placeholder: 'Añadir notas sobre la orden de compra...' + placeholder: 'Añadir requerimientos especiales para el plan...' } ] }] : []) ]} onEdit={() => { - console.log('Editing purchase order:', selectedOrder.id); + console.log('Editing procurement plan:', selectedPlan.id); }} /> )} diff --git a/gateway/app/main.py b/gateway/app/main.py index 98c30f2a..eb7a7e78 100644 --- a/gateway/app/main.py +++ b/gateway/app/main.py @@ -65,6 +65,7 @@ app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"]) app.include_router(notification.router, prefix="/api/v1/notifications", tags=["notifications"]) app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location"]) + @app.on_event("startup") async def startup_event(): """Application startup""" diff --git a/gateway/app/routes/tenant.py b/gateway/app/routes/tenant.py index 3a836cb0..4071cdaf 100644 --- a/gateway/app/routes/tenant.py +++ b/gateway/app/routes/tenant.py @@ -143,6 +143,12 @@ async def proxy_tenant_notifications(request: Request, tenant_id: str = Path(... # TENANT-SCOPED INVENTORY SERVICE ENDPOINTS # ================================================================ +@router.api_route("/{tenant_id}/alerts{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) +async def proxy_tenant_alerts(request: Request, tenant_id: str = Path(...), path: str = ""): + """Proxy tenant alerts requests to inventory service""" + target_path = f"/api/v1/tenants/{tenant_id}/alerts{path}".rstrip("/") + return await _proxy_to_inventory_service(request, target_path, tenant_id=tenant_id) + @router.api_route("/{tenant_id}/inventory/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) async def proxy_tenant_inventory(request: Request, tenant_id: str = Path(...), path: str = ""): """Proxy tenant inventory requests to inventory service""" @@ -179,6 +185,18 @@ async def proxy_tenant_orders(request: Request, tenant_id: str = Path(...), path target_path = f"/api/v1/tenants/{tenant_id}/orders/{path}".rstrip("/") return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id) +@router.api_route("/{tenant_id}/customers/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) +async def proxy_tenant_customers(request: Request, tenant_id: str = Path(...), path: str = ""): + """Proxy tenant customers requests to orders service""" + target_path = f"/api/v1/tenants/{tenant_id}/customers/{path}".rstrip("/") + return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id) + +@router.api_route("/{tenant_id}/procurement/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) +async def proxy_tenant_procurement(request: Request, tenant_id: str = Path(...), path: str = ""): + """Proxy tenant procurement requests to orders service""" + target_path = f"/api/v1/tenants/{tenant_id}/procurement/{path}".rstrip("/") + return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id) + # ================================================================ # TENANT-SCOPED SUPPLIER SERVICE ENDPOINTS # ================================================================ diff --git a/services/orders/app/api/procurement.py b/services/orders/app/api/procurement.py index f569605f..c958f281 100644 --- a/services/orders/app/api/procurement.py +++ b/services/orders/app/api/procurement.py @@ -27,8 +27,8 @@ from shared.clients.forecast_client import ForecastServiceClient from shared.config.base import BaseServiceSettings from shared.monitoring.decorators import monitor_performance -# Create router -router = APIRouter(prefix="/procurement-plans", tags=["Procurement Planning"]) +# Create router - tenant-scoped +router = APIRouter(prefix="/tenants/{tenant_id}", tags=["Procurement Planning"]) # Create service settings service_settings = BaseServiceSettings() @@ -82,9 +82,10 @@ async def get_procurement_service(db: AsyncSession = Depends(get_db)) -> Procure # PROCUREMENT PLAN ENDPOINTS # ================================================================ -@router.get("/current", response_model=Optional[ProcurementPlanResponse]) +@router.get("/procurement/plans/current", response_model=Optional[ProcurementPlanResponse]) @monitor_performance("get_current_procurement_plan") async def get_current_procurement_plan( + tenant_id: uuid.UUID, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) ): @@ -103,9 +104,10 @@ async def get_current_procurement_plan( ) -@router.get("/{plan_date}", response_model=Optional[ProcurementPlanResponse]) +@router.get("/procurement/plans/date/{plan_date}", response_model=Optional[ProcurementPlanResponse]) @monitor_performance("get_procurement_plan_by_date") async def get_procurement_plan_by_date( + tenant_id: uuid.UUID, plan_date: date, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) @@ -125,9 +127,10 @@ async def get_procurement_plan_by_date( ) -@router.get("/", response_model=PaginatedProcurementPlans) +@router.get("/procurement/plans", response_model=PaginatedProcurementPlans) @monitor_performance("list_procurement_plans") async def list_procurement_plans( + tenant_id: uuid.UUID, status: Optional[str] = Query(None, description="Filter by plan status"), start_date: Optional[date] = Query(None, description="Start date filter (YYYY-MM-DD)"), end_date: Optional[date] = Query(None, description="End date filter (YYYY-MM-DD)"), @@ -175,9 +178,10 @@ async def list_procurement_plans( ) -@router.post("/generate", response_model=GeneratePlanResponse) +@router.post("/procurement/plans/generate", response_model=GeneratePlanResponse) @monitor_performance("generate_procurement_plan") async def generate_procurement_plan( + tenant_id: uuid.UUID, request: GeneratePlanRequest, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) @@ -216,9 +220,10 @@ async def generate_procurement_plan( ) -@router.put("/{plan_id}/status") +@router.put("/procurement/plans/{plan_id}/status") @monitor_performance("update_procurement_plan_status") async def update_procurement_plan_status( + tenant_id: uuid.UUID, plan_id: uuid.UUID, status: str = Query(..., description="New status", pattern="^(draft|pending_approval|approved|in_execution|completed|cancelled)$"), tenant_access: TenantAccess = Depends(get_current_tenant), @@ -254,9 +259,10 @@ async def update_procurement_plan_status( ) -@router.get("/id/{plan_id}", response_model=Optional[ProcurementPlanResponse]) +@router.get("/procurement/plans/id/{plan_id}", response_model=Optional[ProcurementPlanResponse]) @monitor_performance("get_procurement_plan_by_id") async def get_procurement_plan_by_id( + tenant_id: uuid.UUID, plan_id: uuid.UUID, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) @@ -290,9 +296,10 @@ async def get_procurement_plan_by_id( # DASHBOARD ENDPOINTS # ================================================================ -@router.get("/dashboard/data", response_model=Optional[DashboardData]) +@router.get("/procurement/dashboard", response_model=Optional[DashboardData]) @monitor_performance("get_procurement_dashboard") async def get_procurement_dashboard( + tenant_id: uuid.UUID, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) ): @@ -321,9 +328,10 @@ async def get_procurement_dashboard( # REQUIREMENT MANAGEMENT ENDPOINTS # ================================================================ -@router.get("/{plan_id}/requirements") +@router.get("/procurement/plans/{plan_id}/requirements") @monitor_performance("get_plan_requirements") async def get_plan_requirements( + tenant_id: uuid.UUID, plan_id: uuid.UUID, status: Optional[str] = Query(None, description="Filter by requirement status"), priority: Optional[str] = Query(None, description="Filter by priority level"), @@ -364,9 +372,10 @@ async def get_plan_requirements( ) -@router.get("/requirements/critical") +@router.get("/procurement/requirements/critical") @monitor_performance("get_critical_requirements") async def get_critical_requirements( + tenant_id: uuid.UUID, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) ): @@ -391,9 +400,10 @@ async def get_critical_requirements( # UTILITY ENDPOINTS # ================================================================ -@router.post("/scheduler/trigger") +@router.post("/procurement/scheduler/trigger") @monitor_performance("trigger_daily_scheduler") async def trigger_daily_scheduler( + tenant_id: uuid.UUID, tenant_access: TenantAccess = Depends(get_current_tenant), procurement_service: ProcurementService = Depends(get_procurement_service) ): @@ -419,7 +429,7 @@ async def trigger_daily_scheduler( ) -@router.get("/health") +@router.get("/procurement/health") async def procurement_health_check(): """ Health check endpoint for procurement service diff --git a/services/orders/app/repositories/procurement_repository.py b/services/orders/app/repositories/procurement_repository.py index 4311f20b..900acf7d 100644 --- a/services/orders/app/repositories/procurement_repository.py +++ b/services/orders/app/repositories/procurement_repository.py @@ -21,7 +21,8 @@ class ProcurementPlanRepository(BaseRepository): """Repository for procurement plan operations""" def __init__(self, db: AsyncSession): - super().__init__(db, ProcurementPlan) + super().__init__(ProcurementPlan) + self.db = db async def create_plan(self, plan_data: Dict[str, Any]) -> ProcurementPlan: """Create a new procurement plan""" @@ -134,7 +135,8 @@ class ProcurementRequirementRepository(BaseRepository): """Repository for procurement requirement operations""" def __init__(self, db: AsyncSession): - super().__init__(db, ProcurementRequirement) + super().__init__(ProcurementRequirement) + self.db = db async def create_requirement(self, requirement_data: Dict[str, Any]) -> ProcurementRequirement: """Create a new procurement requirement""" diff --git a/services/orders/app/services/cache_service.py b/services/orders/app/services/cache_service.py index e922828f..d64d53fc 100644 --- a/services/orders/app/services/cache_service.py +++ b/services/orders/app/services/cache_service.py @@ -36,7 +36,7 @@ class CacheService: self.redis_url, decode_responses=True, socket_keepalive=True, - socket_keepalive_options={"TCP_KEEPIDLE": 1, "TCP_KEEPINTVL": 3, "TCP_KEEPCNT": 5}, + socket_keepalive_options={1: 1, 3: 3, 5: 5}, # Use integer keys retry_on_timeout=True, max_connections=50 )