Add fixes to procurement logic and fix rel-time connections

This commit is contained in:
Urtzi Alfaro
2025-10-02 13:20:30 +02:00
parent c9d8d1d071
commit 1243c2ca6d
24 changed files with 4984 additions and 348 deletions

View File

@@ -8,6 +8,7 @@ import StatsGrid from '../../components/ui/Stats/StatsGrid';
import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts';
import ProcurementPlansToday from '../../components/domain/dashboard/ProcurementPlansToday';
import ProductionPlansToday from '../../components/domain/dashboard/ProductionPlansToday';
import PurchaseOrdersTracking from '../../components/domain/dashboard/PurchaseOrdersTracking';
import { useTenant } from '../../stores/tenant.store';
import {
AlertTriangle,
@@ -154,14 +155,17 @@ const DashboardPage: React.FC = () => {
{/* 1. Real-time alerts block */}
<RealTimeAlerts />
{/* 2. Procurement plans block */}
{/* 2. Purchase Orders Tracking block */}
<PurchaseOrdersTracking />
{/* 3. Procurement plans block */}
<ProcurementPlansToday
onOrderItem={handleOrderItem}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
/>
{/* 3. Production plans block */}
{/* 4. Production plans block */}
<ProductionPlansToday
onStartOrder={handleStartOrder}
onPauseOrder={handlePauseOrder}

View File

@@ -0,0 +1,472 @@
import React, { useState } from 'react';
import {
ShoppingCart,
TrendingUp,
AlertCircle,
Target,
DollarSign,
Award,
Lock,
BarChart3,
Package,
Truck,
Calendar
} from 'lucide-react';
import { PageHeader } from '../../../components/layout';
import { Card, StatsGrid, Button, Tabs } from '../../../components/ui';
import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProcurementDashboard } from '../../../api/hooks/orders';
import { formatters } from '../../../components/ui/Stats/StatsPresets';
const ProcurementAnalyticsPage: React.FC = () => {
const { canAccessAnalytics } = useSubscription();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const [activeTab, setActiveTab] = useState('overview');
const { data: dashboard, isLoading: dashboardLoading } = useProcurementDashboard(tenantId);
// Check if user has access to advanced analytics (professional/enterprise)
const hasAdvancedAccess = canAccessAnalytics('advanced');
// If user doesn't have access to advanced analytics, show upgrade message
if (!hasAdvancedAccess) {
return (
<div className="space-y-6">
<PageHeader
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
/>
<Card className="p-8 text-center">
<Lock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Funcionalidad Exclusiva para Profesionales y Empresas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
La analítica avanzada de compras está disponible solo para planes Professional y Enterprise.
Actualiza tu plan para acceder a análisis detallados de proveedores, optimización de costos y métricas de rendimiento.
</p>
<Button
variant="primary"
size="md"
onClick={() => window.location.hash = '#/app/settings/profile'}
>
Actualizar Plan
</Button>
</Card>
</div>
);
}
// Tab configuration
const tabs = [
{
id: 'overview',
label: 'Resumen',
icon: BarChart3
},
{
id: 'performance',
label: 'Rendimiento',
icon: TrendingUp
},
{
id: 'suppliers',
label: 'Proveedores',
icon: Truck
},
{
id: 'costs',
label: 'Costos',
icon: DollarSign
},
{
id: 'quality',
label: 'Calidad',
icon: Award
}
];
return (
<div className="space-y-6">
<PageHeader
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
/>
{/* Summary Stats */}
<StatsGrid
stats={[
{
label: 'Planes Activos',
value: dashboard?.stats?.total_plans || 0,
icon: ShoppingCart,
formatter: formatters.number
},
{
label: 'Tasa de Cumplimiento',
value: dashboard?.stats?.avg_fulfillment_rate || 0,
icon: Target,
formatter: formatters.percentage,
change: dashboard?.stats?.fulfillment_trend
},
{
label: 'Entregas a Tiempo',
value: dashboard?.stats?.avg_on_time_delivery || 0,
icon: Calendar,
formatter: formatters.percentage,
change: dashboard?.stats?.on_time_trend
},
{
label: 'Variación de Costos',
value: dashboard?.stats?.avg_cost_variance || 0,
icon: DollarSign,
formatter: formatters.percentage,
change: dashboard?.stats?.cost_variance_trend
}
]}
loading={dashboardLoading}
/>
{/* Tabs */}
<Tabs
items={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
{/* Tab Content */}
<div className="space-y-6">
{activeTab === 'overview' && (
<>
{/* Overview Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Plan Status Distribution */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Distribución de Estados de Planes
</h3>
<div className="space-y-3">
{dashboard?.plan_status_distribution?.map((status: any) => (
<div key={status.status} className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{status.status}</span>
<div className="flex items-center gap-2">
<div className="w-32 h-2 bg-[var(--bg-tertiary)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-primary)]"
style={{ width: `${(status.count / dashboard.stats.total_plans) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-8 text-right">
{status.count}
</span>
</div>
</div>
))}
</div>
</div>
</Card>
{/* Critical Requirements */}
<Card>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-[var(--text-primary)]">
Requerimientos Críticos
</h3>
<AlertCircle className="h-5 w-5 text-[var(--color-error)]" />
</div>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Stock Crítico</span>
<span className="text-2xl font-bold text-[var(--color-error)]">
{dashboard?.critical_requirements?.low_stock || 0}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Entregas Atrasadas</span>
<span className="text-2xl font-bold text-[var(--color-warning)]">
{dashboard?.critical_requirements?.overdue || 0}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Alta Prioridad</span>
<span className="text-2xl font-bold text-[var(--color-info)]">
{dashboard?.critical_requirements?.high_priority || 0}
</span>
</div>
</div>
</div>
</Card>
</div>
{/* Recent Plans */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Planes Recientes
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[var(--border-primary)]">
<th className="text-left py-3 px-4 text-[var(--text-secondary)] font-medium">Plan</th>
<th className="text-left py-3 px-4 text-[var(--text-secondary)] font-medium">Fecha</th>
<th className="text-left py-3 px-4 text-[var(--text-secondary)] font-medium">Estado</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Requerimientos</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Costo Total</th>
</tr>
</thead>
<tbody>
{dashboard?.recent_plans?.map((plan: any) => (
<tr key={plan.id} className="border-b border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]">
<td className="py-3 px-4 text-[var(--text-primary)]">{plan.plan_number}</td>
<td className="py-3 px-4 text-[var(--text-secondary)]">
{new Date(plan.plan_date).toLocaleDateString()}
</td>
<td className="py-3 px-4">
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(plan.status)}`}>
{plan.status}
</span>
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{plan.total_requirements}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.currency(plan.total_estimated_cost)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
</>
)}
{activeTab === 'performance' && (
<>
{/* Performance Tab */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<div className="p-6 text-center">
<Target className="mx-auto h-8 w-8 text-[var(--color-success)] mb-3" />
<div className="text-3xl font-bold text-[var(--text-primary)] mb-1">
{formatters.percentage(dashboard?.stats?.avg_fulfillment_rate || 0)}
</div>
<div className="text-sm text-[var(--text-secondary)]">Tasa de Cumplimiento</div>
</div>
</Card>
<Card>
<div className="p-6 text-center">
<Calendar className="mx-auto h-8 w-8 text-[var(--color-info)] mb-3" />
<div className="text-3xl font-bold text-[var(--text-primary)] mb-1">
{formatters.percentage(dashboard?.stats?.avg_on_time_delivery || 0)}
</div>
<div className="text-sm text-[var(--text-secondary)]">Entregas a Tiempo</div>
</div>
</Card>
<Card>
<div className="p-6 text-center">
<Award className="mx-auto h-8 w-8 text-[var(--color-warning)] mb-3" />
<div className="text-3xl font-bold text-[var(--text-primary)] mb-1">
{dashboard?.stats?.avg_quality_score?.toFixed(1) || '0.0'}
</div>
<div className="text-sm text-[var(--text-secondary)]">Puntuación de Calidad</div>
</div>
</Card>
</div>
{/* Performance Trend Chart Placeholder */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Tendencias de Rendimiento
</h3>
<div className="h-64 flex items-center justify-center text-[var(--text-tertiary)]">
Gráfico de tendencias - Próximamente
</div>
</div>
</Card>
</>
)}
{activeTab === 'suppliers' && (
<>
{/* Suppliers Tab */}
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Rendimiento de Proveedores
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[var(--border-primary)]">
<th className="text-left py-3 px-4 text-[var(--text-secondary)] font-medium">Proveedor</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Órdenes</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Tasa Cumplimiento</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Entregas a Tiempo</th>
<th className="text-right py-3 px-4 text-[var(--text-secondary)] font-medium">Calidad</th>
</tr>
</thead>
<tbody>
{dashboard?.supplier_performance?.map((supplier: any) => (
<tr key={supplier.id} className="border-b border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]">
<td className="py-3 px-4 text-[var(--text-primary)]">{supplier.name}</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">{supplier.total_orders}</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.percentage(supplier.fulfillment_rate)}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{formatters.percentage(supplier.on_time_rate)}
</td>
<td className="py-3 px-4 text-right text-[var(--text-primary)]">
{supplier.quality_score?.toFixed(1) || 'N/A'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Card>
</>
)}
{activeTab === 'costs' && (
<>
{/* Costs Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Análisis de Costos
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Costo Total Estimado</span>
<span className="text-2xl font-bold text-[var(--text-primary)]">
{formatters.currency(dashboard?.cost_analysis?.total_estimated || 0)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Costo Total Aprobado</span>
<span className="text-2xl font-bold text-[var(--text-primary)]">
{formatters.currency(dashboard?.cost_analysis?.total_approved || 0)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Variación Promedio</span>
<span className={`text-2xl font-bold ${
(dashboard?.cost_analysis?.avg_variance || 0) > 0
? 'text-[var(--color-error)]'
: 'text-[var(--color-success)]'
}`}>
{formatters.percentage(Math.abs(dashboard?.cost_analysis?.avg_variance || 0))}
</span>
</div>
</div>
</div>
</Card>
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Distribución de Costos por Categoría
</h3>
<div className="space-y-3">
{dashboard?.cost_by_category?.map((category: any) => (
<div key={category.name} className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{category.name}</span>
<div className="flex items-center gap-2">
<div className="w-32 h-2 bg-[var(--bg-tertiary)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-primary)]"
style={{ width: `${(category.amount / dashboard.cost_analysis.total_estimated) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-20 text-right">
{formatters.currency(category.amount)}
</span>
</div>
</div>
))}
</div>
</div>
</Card>
</div>
</>
)}
{activeTab === 'quality' && (
<>
{/* Quality Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Métricas de Calidad
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Puntuación Promedio</span>
<span className="text-3xl font-bold text-[var(--text-primary)]">
{dashboard?.quality_metrics?.avg_score?.toFixed(1) || '0.0'} / 10
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Productos con Calidad Alta</span>
<span className="text-2xl font-bold text-[var(--color-success)]">
{dashboard?.quality_metrics?.high_quality_count || 0}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">Productos con Calidad Baja</span>
<span className="text-2xl font-bold text-[var(--color-error)]">
{dashboard?.quality_metrics?.low_quality_count || 0}
</span>
</div>
</div>
</div>
</Card>
<Card>
<div className="p-6">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-4">
Tendencia de Calidad
</h3>
<div className="h-48 flex items-center justify-center text-[var(--text-tertiary)]">
Gráfico de tendencia de calidad - Próximamente
</div>
</div>
</Card>
</div>
</>
)}
</div>
</div>
);
};
// Helper function for status colors
function getStatusColor(status: string): string {
const colors: Record<string, string> = {
draft: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]',
pending_approval: 'bg-yellow-100 text-yellow-800',
approved: 'bg-green-100 text-green-800',
in_execution: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
cancelled: 'bg-red-100 text-red-800'
};
return colors[status] || colors.draft;
}
export default ProcurementAnalyticsPage;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Plus, ShoppingCart, Truck, Euro, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play, Zap, User } from 'lucide-react';
import { Button, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
import { Button, Card, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, Input, type FilterConfig } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { CreatePurchaseOrderModal } from '../../../../components/domain/procurement/CreatePurchaseOrderModal';
@@ -10,7 +10,13 @@ import {
usePlanRequirements,
useGenerateProcurementPlan,
useUpdateProcurementPlanStatus,
useTriggerDailyScheduler
useTriggerDailyScheduler,
useRecalculateProcurementPlan,
useApproveProcurementPlan,
useRejectProcurementPlan,
useCreatePurchaseOrdersFromPlan,
useLinkRequirementToPurchaseOrder,
useUpdateRequirementDeliveryStatus
} from '../../../../api';
import { useTenantStore } from '../../../../stores/tenant.store';
@@ -38,6 +44,20 @@ const ProcurementPage: React.FC = () => {
force_regenerate: false
});
// New feature state
const [showApprovalModal, setShowApprovalModal] = useState(false);
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>('approve');
const [approvalNotes, setApprovalNotes] = useState('');
const [planForApproval, setPlanForApproval] = useState<any>(null);
const [showDeliveryUpdateModal, setShowDeliveryUpdateModal] = useState(false);
const [requirementForDelivery, setRequirementForDelivery] = useState<any>(null);
const [deliveryUpdateForm, setDeliveryUpdateForm] = useState({
delivery_status: 'pending',
received_quantity: 0,
actual_delivery_date: '',
quality_rating: 5
});
// Requirement details functionality
const handleViewRequirementDetails = (requirement: any) => {
@@ -82,6 +102,13 @@ const ProcurementPage: React.FC = () => {
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
const triggerSchedulerMutation = useTriggerDailyScheduler();
// New feature mutations
const recalculatePlanMutation = useRecalculateProcurementPlan();
const approvePlanMutation = useApproveProcurementPlan();
const rejectPlanMutation = useRejectProcurementPlan();
const createPOsMutation = useCreatePurchaseOrdersFromPlan();
const updateDeliveryMutation = useUpdateRequirementDeliveryStatus();
// Helper functions for stage transitions and edit functionality
const getNextStage = (currentStatus: string): string | null => {
const stageFlow: { [key: string]: string } = {
@@ -158,6 +185,96 @@ const ProcurementPage: React.FC = () => {
setSelectedPlanForRequirements(null);
};
// NEW FEATURE HANDLERS
const handleRecalculatePlan = (plan: any) => {
if (window.confirm('¿Recalcular el plan con el inventario actual? Esto puede cambiar las cantidades requeridas.')) {
recalculatePlanMutation.mutate({ tenantId, planId: plan.id });
}
};
const handleOpenApprovalModal = (plan: any, action: 'approve' | 'reject') => {
setPlanForApproval(plan);
setApprovalAction(action);
setApprovalNotes('');
setShowApprovalModal(true);
};
const handleConfirmApproval = () => {
if (!planForApproval) return;
if (approvalAction === 'approve') {
approvePlanMutation.mutate({
tenantId,
planId: planForApproval.id,
approval_notes: approvalNotes || undefined
}, {
onSuccess: () => {
setShowApprovalModal(false);
setPlanForApproval(null);
setApprovalNotes('');
}
});
} else {
rejectPlanMutation.mutate({
tenantId,
planId: planForApproval.id,
rejection_notes: approvalNotes || undefined
}, {
onSuccess: () => {
setShowApprovalModal(false);
setPlanForApproval(null);
setApprovalNotes('');
}
});
}
};
const handleCreatePurchaseOrders = (plan: any) => {
if (plan.status !== 'approved') {
alert('El plan debe estar aprobado antes de crear órdenes de compra');
return;
}
if (window.confirm(`¿Crear órdenes de compra automáticamente para ${plan.total_requirements} requerimientos?`)) {
createPOsMutation.mutate({
tenantId,
planId: plan.id,
autoApprove: false
});
}
};
const handleOpenDeliveryUpdate = (requirement: any) => {
setRequirementForDelivery(requirement);
setDeliveryUpdateForm({
delivery_status: requirement.delivery_status || 'pending',
received_quantity: requirement.received_quantity || 0,
actual_delivery_date: requirement.actual_delivery_date || '',
quality_rating: requirement.quality_rating || 5
});
setShowDeliveryUpdateModal(true);
};
const handleConfirmDeliveryUpdate = () => {
if (!requirementForDelivery) return;
updateDeliveryMutation.mutate({
tenantId,
requirementId: requirementForDelivery.id,
request: {
delivery_status: deliveryUpdateForm.delivery_status,
received_quantity: deliveryUpdateForm.received_quantity || undefined,
actual_delivery_date: deliveryUpdateForm.actual_delivery_date || undefined,
quality_rating: deliveryUpdateForm.quality_rating || undefined
}
}, {
onSuccess: () => {
setShowDeliveryUpdateModal(false);
setRequirementForDelivery(null);
}
});
};
if (!tenantId) {
return (
<div className="flex justify-center items-center h-64">
@@ -413,8 +530,74 @@ const ProcurementPage: React.FC = () => {
}
);
} else {
// Primary action: Stage transition (most important)
if (nextStageConfig) {
// NEW FEATURES: Recalculate and Approval actions for draft/pending
if (plan.status === 'draft') {
const planAgeHours = (new Date().getTime() - new Date(plan.created_at).getTime()) / (1000 * 60 * 60);
actions.push({
label: planAgeHours > 24 ? '⚠️ Recalcular' : 'Recalcular',
icon: ArrowRight,
variant: 'outline' as const,
priority: 'primary' as const,
onClick: () => handleRecalculatePlan(plan)
});
actions.push({
label: 'Aprobar',
icon: CheckCircle,
variant: 'primary' as const,
priority: 'primary' as const,
onClick: () => handleOpenApprovalModal(plan, 'approve')
});
actions.push({
label: 'Rechazar',
icon: X,
variant: 'outline' as const,
priority: 'secondary' as const,
destructive: true,
onClick: () => handleOpenApprovalModal(plan, 'reject')
});
} else if (plan.status === 'pending_approval') {
actions.push({
label: 'Aprobar',
icon: CheckCircle,
variant: 'primary' as const,
priority: 'primary' as const,
onClick: () => handleOpenApprovalModal(plan, 'approve')
});
actions.push({
label: 'Rechazar',
icon: X,
variant: 'outline' as const,
priority: 'secondary' as const,
destructive: true,
onClick: () => handleOpenApprovalModal(plan, 'reject')
});
}
// NEW FEATURE: Auto-create POs for approved plans
if (plan.status === 'approved') {
actions.push({
label: 'Crear Órdenes de Compra',
icon: ShoppingCart,
variant: 'primary' as const,
priority: 'primary' as const,
onClick: () => handleCreatePurchaseOrders(plan)
});
actions.push({
label: 'Iniciar Ejecución',
icon: Play,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => handleStageTransition(plan.id, plan.status)
});
}
// Original stage transition for other statuses
if (nextStageConfig && !['draft', 'pending_approval', 'approved'].includes(plan.status)) {
actions.push({
label: nextStageConfig.label,
icon: nextStageConfig.icon,
@@ -459,7 +642,7 @@ const ProcurementPage: React.FC = () => {
// Tertiary action: Cancel (least prominent, destructive)
if (!['completed', 'cancelled'].includes(plan.status)) {
actions.push({
label: 'Cancelar',
label: 'Cancelar Plan',
icon: X,
variant: 'outline' as const,
priority: 'tertiary' as const,
@@ -1281,6 +1464,229 @@ const ProcurementPage: React.FC = () => {
</div>
</div>
)}
{/* NEW FEATURE MODALS */}
{/* Approval/Rejection Modal */}
{showApprovalModal && planForApproval && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl w-full max-w-md mx-4">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[var(--border-primary)]">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${approvalAction === 'approve' ? 'bg-green-100' : 'bg-red-100'}`}>
{approvalAction === 'approve' ? (
<CheckCircle className={`w-5 h-5 text-green-600`} />
) : (
<X className={`w-5 h-5 text-red-600`} />
)}
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
{approvalAction === 'approve' ? 'Aprobar Plan' : 'Rechazar Plan'}
</h2>
<p className="text-sm text-[var(--text-secondary)]">
Plan {planForApproval.plan_number}
</p>
</div>
</div>
<Button
variant="outline"
onClick={() => setShowApprovalModal(false)}
className="p-2"
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Notas {approvalAction === 'approve' ? '(Opcional)' : '(Requerido)'}
</label>
<textarea
value={approvalNotes}
onChange={(e) => setApprovalNotes(e.target.value)}
placeholder={approvalAction === 'approve' ? 'Razón de aprobación...' : 'Razón de rechazo...'}
className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-[var(--text-tertiary)]
focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]
transition-colors duration-200 resize-vertical min-h-[100px]"
rows={4}
/>
</div>
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-2">Detalles del Plan</h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Requerimientos:</span>
<span className="text-[var(--text-primary)] font-medium">{planForApproval.total_requirements}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Costo Estimado:</span>
<span className="text-[var(--text-primary)] font-medium">{planForApproval.total_estimated_cost?.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Proveedores:</span>
<span className="text-[var(--text-primary)] font-medium">{planForApproval.primary_suppliers_count || 0}</span>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={() => setShowApprovalModal(false)}
>
Cancelar
</Button>
<Button
variant={approvalAction === 'approve' ? 'primary' : 'outline'}
onClick={handleConfirmApproval}
disabled={approvePlanMutation.isPending || rejectPlanMutation.isPending}
className={approvalAction === 'reject' ? 'bg-red-600 hover:bg-red-700 text-white' : ''}
>
{approvePlanMutation.isPending || rejectPlanMutation.isPending ? (
<>
<Loader className="w-4 h-4 mr-2 animate-spin" />
Procesando...
</>
) : (
<>
{approvalAction === 'approve' ? <CheckCircle className="w-4 h-4 mr-2" /> : <X className="w-4 h-4 mr-2" />}
{approvalAction === 'approve' ? 'Aprobar Plan' : 'Rechazar Plan'}
</>
)}
</Button>
</div>
</div>
</div>
)}
{/* Delivery Status Update Modal */}
{showDeliveryUpdateModal && requirementForDelivery && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl w-full max-w-lg mx-4">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[var(--border-primary)]">
<div className="flex items-center space-x-3">
<div className="p-2 rounded-lg bg-blue-100">
<Truck className="w-5 h-5 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
Actualizar Estado de Entrega
</h2>
<p className="text-sm text-[var(--text-secondary)]">
{requirementForDelivery.product_name}
</p>
</div>
</div>
<Button
variant="outline"
onClick={() => setShowDeliveryUpdateModal(false)}
className="p-2"
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Estado de Entrega
</label>
<select
value={deliveryUpdateForm.delivery_status}
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, delivery_status: e.target.value })}
className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
bg-[var(--bg-primary)] text-[var(--text-primary)]
focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
>
<option value="pending">Pendiente</option>
<option value="in_transit">En Tránsito</option>
<option value="delivered">Entregado</option>
<option value="delayed">Retrasado</option>
<option value="cancelled">Cancelado</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Cantidad Recibida
</label>
<Input
type="number"
value={deliveryUpdateForm.received_quantity}
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, received_quantity: Number(e.target.value) })}
min="0"
step="0.01"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Ordenado: {requirementForDelivery.ordered_quantity || requirementForDelivery.net_requirement} {requirementForDelivery.unit_of_measure}
</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Fecha de Entrega Real
</label>
<Input
type="date"
value={deliveryUpdateForm.actual_delivery_date}
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, actual_delivery_date: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Calificación de Calidad (1-10)
</label>
<Input
type="number"
value={deliveryUpdateForm.quality_rating}
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, quality_rating: Number(e.target.value) })}
min="1"
max="10"
step="0.1"
/>
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={() => setShowDeliveryUpdateModal(false)}
>
Cancelar
</Button>
<Button
variant="primary"
onClick={handleConfirmDeliveryUpdate}
disabled={updateDeliveryMutation.isPending}
>
{updateDeliveryMutation.isPending ? (
<>
<Loader className="w-4 h-4 mr-2 animate-spin" />
Actualizando...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
Actualizar Estado
</>
)}
</Button>
</div>
</div>
</div>
)}
</div>
);
};