Files
bakery-ia/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
2025-09-23 19:24:22 +02:00

1274 lines
55 KiB
TypeScript

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, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { CreatePurchaseOrderModal } from '../../../../components/domain/procurement/CreatePurchaseOrderModal';
import {
useProcurementDashboard,
useProcurementPlans,
usePlanRequirements,
useGenerateProcurementPlan,
useUpdateProcurementPlanStatus,
useTriggerDailyScheduler
} from '../../../../api';
import { useTenantStore } from '../../../../stores/tenant.store';
const ProcurementPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedPlan, setSelectedPlan] = useState<any>(null);
const [editingPlan, setEditingPlan] = useState<any>(null);
const [editFormData, setEditFormData] = useState<any>({});
const [selectedPlanForRequirements, setSelectedPlanForRequirements] = useState<string | null>(null);
const [showCriticalRequirements, setShowCriticalRequirements] = useState(false);
const [showGeneratePlanModal, setShowGeneratePlanModal] = useState(false);
const [showRequirementDetailsModal, setShowRequirementDetailsModal] = useState(false);
const [selectedRequirement, setSelectedRequirement] = useState<any>(null);
const [showCreatePurchaseOrderModal, setShowCreatePurchaseOrderModal] = useState(false);
const [selectedRequirementsForPO, setSelectedRequirementsForPO] = useState<any[]>([]);
const [isAIMode, setIsAIMode] = useState(true);
const [generatePlanForm, setGeneratePlanForm] = useState({
plan_date: new Date().toISOString().split('T')[0],
planning_horizon_days: 14,
include_safety_stock: true,
safety_stock_percentage: 20,
force_regenerate: false
});
// Requirement details functionality
const handleViewRequirementDetails = (requirement: any) => {
setSelectedRequirement(requirement);
setShowRequirementDetailsModal(true);
};
const { currentTenant } = useTenantStore();
const tenantId = currentTenant?.id || '';
// Real API data hooks
const { data: dashboardData, isLoading: isDashboardLoading } = useProcurementDashboard(tenantId);
const { data: procurementPlans, isLoading: isPlansLoading } = useProcurementPlans({
tenant_id: tenantId,
limit: 50,
offset: 0
});
// Get plan requirements for selected plan
const { data: allPlanRequirements, isLoading: isPlanRequirementsLoading } = usePlanRequirements({
tenant_id: tenantId,
plan_id: selectedPlanForRequirements || ''
// Remove status filter to get all requirements
}, {
enabled: !!selectedPlanForRequirements && !!tenantId
});
// Filter critical requirements client-side
const planRequirements = allPlanRequirements?.filter(req => {
// Check various conditions that might make a requirement critical
const isLowStock = req.current_stock_level && req.required_quantity &&
(req.current_stock_level / req.required_quantity) < 0.5;
const isNearDeadline = req.required_by_date &&
(new Date(req.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24) < 7;
const hasHighPriority = req.priority === 'high';
return isLowStock || isNearDeadline || hasHighPriority;
});
const generatePlanMutation = useGenerateProcurementPlan();
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
const triggerSchedulerMutation = useTriggerDailyScheduler();
// Helper functions for stage transitions and edit functionality
const getNextStage = (currentStatus: string): string | null => {
const stageFlow: { [key: string]: string } = {
'draft': 'pending_approval',
'pending_approval': 'approved',
'approved': 'in_execution',
'in_execution': 'completed'
};
return stageFlow[currentStatus] || null;
};
const getStageActionConfig = (status: string) => {
const configs: { [key: string]: { label: string; icon: any; variant: 'primary' | 'outline'; color?: string } } = {
'draft': { label: 'Enviar a Aprobación', icon: ArrowRight, variant: 'primary' },
'pending_approval': { label: 'Aprobar', icon: CheckCircle, variant: 'primary' },
'approved': { label: 'Iniciar Ejecución', icon: Play, variant: 'primary' },
'in_execution': { label: 'Completar', icon: CheckCircle, variant: 'primary' }
};
return configs[status];
};
const canEdit = (status: string): boolean => {
return status === 'draft';
};
const handleStageTransition = (planId: string, currentStatus: string) => {
const nextStage = getNextStage(currentStatus);
if (nextStage) {
updatePlanStatusMutation.mutate({
tenant_id: tenantId,
plan_id: planId,
status: nextStage as any
});
}
};
const handleCancelPlan = (planId: string) => {
updatePlanStatusMutation.mutate({
tenant_id: tenantId,
plan_id: planId,
status: 'cancelled'
});
};
const handleEditPlan = (plan: any) => {
setEditingPlan(plan);
setEditFormData({
special_requirements: plan.special_requirements || '',
planning_horizon_days: plan.planning_horizon_days || 14,
priority: plan.priority || 'medium'
});
};
const handleSaveEdit = () => {
// For now, we'll just update the special requirements since that's the main editable field
// In a real implementation, you might have a separate API endpoint for updating plan details
setEditingPlan(null);
setEditFormData({});
// Here you would typically call an update API
};
const handleCancelEdit = () => {
setEditingPlan(null);
setEditFormData({});
};
const handleShowCriticalRequirements = (planId: string) => {
setSelectedPlanForRequirements(planId);
setShowCriticalRequirements(true);
};
const handleCloseCriticalRequirements = () => {
setShowCriticalRequirements(false);
setSelectedPlanForRequirements(null);
};
if (!tenantId) {
return (
<div className="flex justify-center items-center h-64">
<div className="text-center">
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No hay tenant seleccionado
</h3>
<p className="text-[var(--text-secondary)]">
Selecciona un tenant para ver los datos de procurement
</p>
</div>
</div>
);
}
const getPlanStatusConfig = (status: string) => {
const statusConfig = {
draft: { text: 'Borrador', icon: Clock },
pending_approval: { text: 'Pendiente Aprobación', icon: Clock },
approved: { text: 'Aprobado', 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;
return {
color: getStatusColor(status === 'in_execution' ? 'inTransit' : status === 'pending_approval' ? 'pending' : status),
text: config?.text || status,
icon: Icon,
isCritical: status === 'cancelled',
isHighlight: status === 'pending_approval'
};
};
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 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 procurementStats = [
{
title: 'Planes Totales',
value: stats.totalPlans,
variant: 'default' as const,
icon: Package,
},
{
title: 'Planes Activos',
value: stats.activePlans,
variant: 'success' as const,
icon: CheckCircle,
},
{
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: Euro,
},
{
title: 'Costo Aprobado',
value: formatters.currency(stats.totalApprovedCost),
variant: 'success' as const,
icon: Euro,
},
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between mb-6">
<PageHeader
title="Planificación de Compras"
description="Administra planes de compras, requerimientos y análisis de procurement"
/>
<div className="flex items-center gap-4">
{/* AI/Manual Mode Segmented Control */}
<div className="inline-flex p-1 bg-[var(--surface-secondary)] rounded-xl border border-[var(--border-primary)] shadow-sm">
<button
onClick={() => setIsAIMode(true)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
${isAIMode
? 'bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
}
`}
>
<Zap className="w-4 h-4" />
Automático IA
</button>
<button
onClick={() => setIsAIMode(false)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
${!isAIMode
? 'bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
}
`}
>
<User className="w-4 h-4" />
Manual
</button>
</div>
{/* Action Buttons */}
{!isAIMode && (
<Button
variant="primary"
onClick={() => {
// Open the purchase order modal with empty requirements
// This allows manual creation of purchase orders without procurement plans
setSelectedRequirementsForPO([]);
setShowCreatePurchaseOrderModal(true);
}}
className="flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Crear Orden de Compra
</Button>
)}
{/* Testing button - keep for development */}
<Button
variant="outline"
onClick={() => {
triggerSchedulerMutation.mutate(tenantId, {
onSuccess: (data) => {
// Scheduler executed successfully
// Show success notification (if you have a notification system)
// toast.success(data.message);
},
onError: (error) => {
console.error('❌ Error ejecutando scheduler:', error);
// Show error notification
// toast.error('Error ejecutando el programador de compras');
}
})
}}
disabled={triggerSchedulerMutation.isPending}
className="flex items-center gap-2"
>
{triggerSchedulerMutation.isPending ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Calendar className="w-4 h-4" />
)}
{triggerSchedulerMutation.isPending ? "Ejecutando..." : "Ejecutar Programador"}
</Button>
</div>
</div>
{/* Stats Grid */}
{isDashboardLoading ? (
<div className="flex justify-center items-center h-32">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
</div>
) : (
<StatsGrid
stats={procurementStats}
columns={3}
/>
)}
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar planes por número, estado o notas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
</div>
</Card>
{/* Procurement Plans Grid - Mobile-Optimized */}
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
{isPlansLoading ? (
<div className="col-span-full flex justify-center items-center h-32">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
</div>
) : (
filteredPlans.map((plan) => {
const statusConfig = getPlanStatusConfig(plan.status);
const nextStageConfig = getStageActionConfig(plan.status);
const isEditing = editingPlan?.id === plan.id;
const isEditable = canEdit(plan.status);
// Build actions array with proper priority hierarchy for better UX
const actions = [];
// Edit mode actions (highest priority when editing)
if (isEditing) {
actions.push(
{
label: 'Guardar',
icon: Save,
variant: 'primary' as const,
priority: 'primary' as const,
onClick: handleSaveEdit
},
{
label: 'Cancelar',
icon: X,
variant: 'outline' as const,
priority: 'primary' as const,
destructive: true,
onClick: handleCancelEdit
}
);
} else {
// Primary action: Stage transition (most important)
if (nextStageConfig) {
actions.push({
label: nextStageConfig.label,
icon: nextStageConfig.icon,
variant: nextStageConfig.variant,
priority: 'primary' as const,
onClick: () => handleStageTransition(plan.id, plan.status)
});
}
// Secondary actions: Edit and View
if (isEditable) {
actions.push({
label: 'Editar',
icon: Edit,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => handleEditPlan(plan)
});
}
actions.push({
label: 'Ver',
icon: Eye,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => {
setSelectedPlan(plan);
setModalMode('view');
setShowForm(true);
}
});
// Show Critical Requirements button
actions.push({
label: 'Req. Críticos',
icon: AlertCircle,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => handleShowCriticalRequirements(plan.id)
});
// Tertiary action: Cancel (least prominent, destructive)
if (!['completed', 'cancelled'].includes(plan.status)) {
actions.push({
label: 'Cancelar',
icon: X,
variant: 'outline' as const,
priority: 'tertiary' as const,
destructive: true,
onClick: () => handleCancelPlan(plan.id)
});
}
}
return (
<div key={plan.id} className="space-y-2">
<StatusCard
id={plan.plan_number}
statusIndicator={statusConfig}
title={`Plan ${plan.plan_number}`}
subtitle={`${new Date(plan.plan_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' })} • ${plan.procurement_strategy}`}
primaryValue={plan.total_requirements}
primaryValueLabel="requerimientos"
secondaryInfo={{
label: 'Presupuesto',
value: `${formatters.compact(plan.total_estimated_cost)}`
}}
progress={plan.planning_horizon_days ? {
label: `${plan.planning_horizon_days} días de horizonte`,
percentage: Math.min((plan.planning_horizon_days / 30) * 100, 100),
color: plan.planning_horizon_days > 14 ? '#10b981' : plan.planning_horizon_days > 7 ? '#f59e0b' : '#ef4444'
} : undefined}
metadata={[
`Período: ${new Date(plan.plan_period_start).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
`Creado: ${new Date(plan.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
...(plan.special_requirements ? [`Req. especiales: ${plan.special_requirements}`] : [])
]}
actions={actions}
/>
{/* Inline Edit Form for Draft Plans */}
{isEditing && (
<Card className="p-4 border-l-4 border-l-[var(--color-primary)] bg-[var(--color-primary)]/5">
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
Editando Plan {plan.plan_number}
</h4>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Requerimientos Especiales
</label>
<textarea
value={editFormData.special_requirements}
onChange={(e) => setEditFormData((prev: any) => ({ ...prev, special_requirements: e.target.value }))}
placeholder="Ingrese requerimientos especiales para este plan..."
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-[60px]"
rows={2}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Días de Horizonte
</label>
<Input
type="number"
value={editFormData.planning_horizon_days}
onChange={(e) => setEditFormData((prev: any) => ({ ...prev, planning_horizon_days: parseInt(e.target.value) }))}
min="1"
max="365"
className="w-full"
/>
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Prioridad
</label>
<select
value={editFormData.priority}
onChange={(e) => setEditFormData((prev: any) => ({ ...prev, priority: 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)]
transition-colors duration-200"
>
<option value="low">Baja</option>
<option value="medium">Media</option>
<option value="high">Alta</option>
<option value="urgent">Urgente</option>
</select>
</div>
</div>
</div>
</Card>
)}
</div>
);
})
)
}
</div>
{/* Empty State for Procurement Plans */}
{!isPlansLoading && filteredPlans.length === 0 && (
<div className="text-center py-12">
<Package 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">
No se encontraron planes de compra
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o generar un nuevo plan de compra
</p>
<Button
onClick={() => generatePlanMutation.mutate({
tenantId,
request: {
force_regenerate: false,
planning_horizon_days: 14,
include_safety_stock: true,
safety_stock_percentage: 20
}
})}
disabled={generatePlanMutation.isPending}
>
{generatePlanMutation.isPending ? (
<Loader className="w-4 h-4 mr-2 animate-spin" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
Generar Plan de Compra
</Button>
</div>
)}
{/* Critical Requirements Modal */}
{showCriticalRequirements && selectedPlanForRequirements && (
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden 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="flex items-center justify-center w-8 h-8 rounded-full bg-red-100">
<AlertCircle className="w-5 h-5 text-red-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
Requerimientos Críticos
</h2>
<p className="text-sm text-[var(--text-secondary)]">
Plan {filteredPlans.find(p => p.id === selectedPlanForRequirements)?.plan_number || selectedPlanForRequirements}
</p>
</div>
</div>
<Button
variant="outline"
onClick={handleCloseCriticalRequirements}
className="p-2"
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
{isPlanRequirementsLoading ? (
<div className="flex justify-center items-center h-32">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
</div>
) : planRequirements && planRequirements.length > 0 ? (
<div className="space-y-4">
<div className="flex justify-end mb-4">
<Button
variant="primary"
onClick={() => {
// Set all requirements for PO creation
setSelectedRequirementsForPO(planRequirements);
setShowCreatePurchaseOrderModal(true);
}}
>
<Plus className="w-4 h-4 mr-2" />
Crear Orden de Compra para Todos
</Button>
</div>
{planRequirements.map((requirement) => (
<StatusCard
key={requirement.id}
id={requirement.requirement_number}
statusIndicator={{
color: getStatusColor('danger'),
text: 'Crítico',
icon: AlertCircle,
isCritical: true
}}
title={requirement.product_name}
subtitle={`${requirement.requirement_number}${requirement.supplier_name || 'Sin proveedor'} • Plan: ${filteredPlans.find(p => p.id === selectedPlanForRequirements)?.plan_number || 'N/A'}`}
primaryValue={requirement.required_quantity}
primaryValueLabel={requirement.unit_of_measure}
secondaryInfo={{
label: 'Límite',
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })
}}
progress={requirement.current_stock_level && requirement.required_quantity ? {
label: `${Math.round((requirement.current_stock_level / requirement.required_quantity) * 100)}% cubierto`,
percentage: Math.min((requirement.current_stock_level / requirement.required_quantity) * 100, 100),
color: requirement.current_stock_level >= requirement.required_quantity ? '#10b981' : requirement.current_stock_level >= requirement.required_quantity * 0.5 ? '#f59e0b' : '#ef4444'
} : undefined}
metadata={[
`Stock: ${requirement.current_stock_level || 0} ${requirement.unit_of_measure}`,
`Necesario: ${requirement.required_quantity - (requirement.current_stock_level || 0)} ${requirement.unit_of_measure}`,
`Costo: €${formatters.compact(requirement.estimated_total_cost || 0)}`,
`Días restantes: ${Math.ceil((new Date(requirement.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))}`,
`Estado: ${requirement.status || 'N/A'}`,
`PO: ${requirement.purchase_order_number || 'Sin PO asignado'}`,
`Prioridad: ${requirement.priority || 'N/A'}`
]}
actions={[
{
label: 'Ver Detalles',
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => handleViewRequirementDetails(requirement)
},
...(requirement.purchase_order_number ? [
{
label: 'Ver PO',
icon: Eye,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => {
// TODO: Open purchase order details
}
}
] : [
{
label: 'Crear PO',
icon: Plus,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => {
// Set the single requirement for PO creation
setSelectedRequirementsForPO([requirement]);
setShowCreatePurchaseOrderModal(true);
}
}
]),
{
label: requirement.supplier_name ? 'Cambiar Proveedor' : 'Asignar Proveedor',
icon: Building2,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => {
// TODO: Open supplier assignment modal
}
}
]}
/>
))}
</div>
) : (
<div className="text-center py-12">
<CheckCircle className="mx-auto h-12 w-12 text-green-500 mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No hay requerimientos críticos
</h3>
<p className="text-[var(--text-secondary)]">
Este plan no tiene requerimientos críticos en este momento
</p>
</div>
)}
</div>
</div>
</div>
)}
{/* Procurement Plan Modal */}
{showForm && selectedPlan && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedPlan(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
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 Plan',
icon: Package,
fields: [
{
label: 'Número de Plan',
value: selectedPlan.plan_number,
highlight: true
},
{
label: 'Tipo de Plan',
value: selectedPlan.plan_type
},
{
label: 'Estrategia',
value: selectedPlan.procurement_strategy
},
{
label: 'Prioridad',
value: selectedPlan.priority,
type: 'status'
}
]
},
{
title: 'Fechas y Períodos',
icon: Calendar,
fields: [
{
label: 'Fecha del Plan',
value: selectedPlan.plan_date,
type: 'date'
},
{
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
},
{
label: 'Horizonte de Planificación',
value: `${selectedPlan.planning_horizon_days} días`
}
]
},
{
title: 'Información Financiera',
icon: Euro,
fields: [
{
label: 'Costo Estimado Total',
value: selectedPlan.total_estimated_cost,
type: 'currency',
highlight: true
},
{
label: 'Costo Aprobado Total',
value: selectedPlan.total_approved_cost,
type: 'currency'
},
{
label: 'Varianza de Costo',
value: selectedPlan.cost_variance,
type: 'currency'
}
]
},
{
title: 'Estadísticas',
icon: ShoppingCart,
fields: [
{
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`
}
]
},
...(selectedPlan.special_requirements ? [{
title: 'Requerimientos Especiales',
fields: [
{
label: 'Observaciones',
value: selectedPlan.special_requirements,
span: 2 as const,
editable: true,
placeholder: 'Añadir requerimientos especiales para el plan...'
}
]
}] : [])
]}
onEdit={() => {
// TODO: Implement plan editing functionality
}}
/>
)}
{/* Generate Plan Modal */}
{showGeneratePlanModal && (
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden 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="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100">
<Plus className="w-5 h-5 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
Generar Plan de Compras
</h2>
<p className="text-sm text-[var(--text-secondary)]">
Configura los parámetros para generar un nuevo plan
</p>
</div>
</div>
<Button
variant="outline"
onClick={() => setShowGeneratePlanModal(false)}
className="p-2"
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
<div className="space-y-6">
{/* Plan Date */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Fecha del Plan
</label>
<Input
type="date"
value={generatePlanForm.plan_date}
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, plan_date: e.target.value }))}
className="w-full"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Fecha para la cual se generará el plan de compras
</p>
</div>
{/* Planning Horizon */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Horizonte de Planificación (días)
</label>
<Input
type="number"
min="1"
max="365"
value={generatePlanForm.planning_horizon_days}
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, planning_horizon_days: parseInt(e.target.value) }))}
className="w-full"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Número de días a considerar en la planificación (1-365)
</p>
</div>
{/* Safety Stock */}
<div className="space-y-4">
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="include_safety_stock"
checked={generatePlanForm.include_safety_stock}
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, include_safety_stock: e.target.checked }))}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="include_safety_stock" className="text-sm font-medium text-[var(--text-secondary)]">
Incluir Stock de Seguridad
</label>
</div>
{generatePlanForm.include_safety_stock && (
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Porcentaje de Stock de Seguridad (%)
</label>
<Input
type="number"
min="0"
max="100"
value={generatePlanForm.safety_stock_percentage}
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, safety_stock_percentage: parseInt(e.target.value) }))}
className="w-full"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Porcentaje adicional para stock de seguridad (0-100%)
</p>
</div>
)}
</div>
{/* Force Regenerate */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="force_regenerate"
checked={generatePlanForm.force_regenerate}
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, force_regenerate: e.target.checked }))}
className="w-4 h-4 text-red-600 bg-gray-100 border-gray-300 rounded focus:ring-red-500"
/>
<label htmlFor="force_regenerate" className="text-sm font-medium text-[var(--text-secondary)]">
Forzar Regeneración
</label>
</div>
<p className="text-xs text-[var(--text-tertiary)] ml-7">
Si ya existe un plan para esta fecha, regenerarlo (esto eliminará el plan existente)
</p>
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={() => setShowGeneratePlanModal(false)}
disabled={generatePlanMutation.isPending}
>
Cancelar
</Button>
<Button
variant="primary"
onClick={() => {
generatePlanMutation.mutate({
tenantId,
request: {
plan_date: generatePlanForm.plan_date,
planning_horizon_days: generatePlanForm.planning_horizon_days,
include_safety_stock: generatePlanForm.include_safety_stock,
safety_stock_percentage: generatePlanForm.safety_stock_percentage,
force_regenerate: generatePlanForm.force_regenerate
}
}, {
onSuccess: () => {
setShowGeneratePlanModal(false);
// Reset form to defaults
setGeneratePlanForm({
plan_date: new Date().toISOString().split('T')[0],
planning_horizon_days: 14,
include_safety_stock: true,
safety_stock_percentage: 20,
force_regenerate: false
});
}
});
}}
disabled={generatePlanMutation.isPending}
>
{generatePlanMutation.isPending ? (
<>
<Loader className="w-4 h-4 mr-2 animate-spin" />
Generando...
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
Generar Plan
</>
)}
</Button>
</div>
</div>
</div>
)}
{/* Create Purchase Order Modal */}
{showCreatePurchaseOrderModal && (
<CreatePurchaseOrderModal
isOpen={showCreatePurchaseOrderModal}
onClose={() => {
setShowCreatePurchaseOrderModal(false);
setSelectedRequirementsForPO([]);
}}
requirements={selectedRequirementsForPO}
onSuccess={() => {
// Refresh the plan requirements data
setSelectedRequirementsForPO([]);
// You might want to invalidate queries here to refresh data
}}
/>
)}
{/* Requirement Details Modal */}
{showRequirementDetailsModal && selectedRequirement && (
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden 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="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100">
<Eye className="w-5 h-5 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
Detalles del Requerimiento
</h2>
<p className="text-sm text-[var(--text-secondary)]">
{selectedRequirement.product_name}
</p>
</div>
</div>
<Button
variant="outline"
onClick={() => setShowRequirementDetailsModal(false)}
className="p-2"
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Product Information */}
<div className="space-y-4">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Información del Producto
</h3>
<div className="space-y-2">
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Nombre</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.product_name}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">SKU</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.product_sku || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Categoría</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.product_category || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Tipo</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.product_type}</p>
</div>
</div>
</div>
{/* Quantities */}
<div className="space-y-4">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Cantidades
</h3>
<div className="space-y-2">
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Cantidad Requerida</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.required_quantity} {selectedRequirement.unit_of_measure}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Stock de Seguridad</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.safety_stock_quantity} {selectedRequirement.unit_of_measure}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Stock Actual</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.current_stock_level} {selectedRequirement.unit_of_measure}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Requerimiento Neto</label>
<p className="text-[var(--text-primary)] font-semibold">{selectedRequirement.net_requirement} {selectedRequirement.unit_of_measure}</p>
</div>
</div>
</div>
{/* Costs */}
<div className="space-y-4">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Costos
</h3>
<div className="space-y-2">
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Costo Unitario Estimado</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.estimated_unit_cost || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Costo Total Estimado</label>
<p className="text-[var(--text-primary)] font-semibold">{selectedRequirement.estimated_total_cost || 'N/A'}</p>
</div>
{selectedRequirement.last_purchase_cost && (
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Último Precio de Compra</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.last_purchase_cost}</p>
</div>
)}
</div>
</div>
{/* Dates & Timeline */}
<div className="space-y-4">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Fechas
</h3>
<div className="space-y-2">
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Requerido Para</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.required_by_date}</p>
</div>
{selectedRequirement.suggested_order_date && (
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Fecha Sugerida de Pedido</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.suggested_order_date}</p>
</div>
)}
{selectedRequirement.latest_order_date && (
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Fecha Límite de Pedido</label>
<p className="text-[var(--text-primary)] text-red-600">{selectedRequirement.latest_order_date}</p>
</div>
)}
</div>
</div>
{/* Status & Priority */}
<div className="space-y-4">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Estado y Prioridad
</h3>
<div className="space-y-2">
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Estado</label>
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
selectedRequirement.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
selectedRequirement.status === 'approved' ? 'bg-green-100 text-green-800' :
selectedRequirement.status === 'ordered' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{selectedRequirement.status}
</span>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Prioridad</label>
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
selectedRequirement.priority === 'critical' ? 'bg-red-100 text-red-800' :
selectedRequirement.priority === 'high' ? 'bg-orange-100 text-orange-800' :
'bg-green-100 text-green-800'
}`}>
{selectedRequirement.priority}
</span>
</div>
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Nivel de Riesgo</label>
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
selectedRequirement.risk_level === 'high' ? 'bg-red-100 text-red-800' :
selectedRequirement.risk_level === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{selectedRequirement.risk_level}
</span>
</div>
</div>
</div>
{/* Supplier Information */}
{selectedRequirement.supplier_name && (
<div className="space-y-4">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Proveedor
</h3>
<div className="space-y-2">
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Nombre</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.supplier_name}</p>
</div>
{selectedRequirement.supplier_lead_time_days && (
<div>
<label className="text-sm font-medium text-[var(--text-secondary)]">Tiempo de Entrega</label>
<p className="text-[var(--text-primary)]">{selectedRequirement.supplier_lead_time_days} días</p>
</div>
)}
</div>
</div>
)}
</div>
{/* Special Requirements */}
{selectedRequirement.special_requirements && (
<div className="mt-6 space-y-2">
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
Requerimientos Especiales
</h3>
<p className="text-[var(--text-primary)] bg-gray-50 p-3 rounded-lg">
{selectedRequirement.special_requirements}
</p>
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={() => setShowRequirementDetailsModal(false)}
>
Cerrar
</Button>
{selectedRequirement.status === 'pending' && (
<Button
variant="primary"
onClick={() => {
// TODO: Implement approval functionality
setShowRequirementDetailsModal(false);
}}
>
<CheckCircle className="w-4 h-4 mr-2" />
Aprobar
</Button>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default ProcurementPage;