Imporve the AI models page

This commit is contained in:
Urtzi Alfaro
2025-09-20 23:30:54 +02:00
parent 38d314e28d
commit 67b8fcdf9f
6 changed files with 685 additions and 571 deletions

View File

@@ -1,13 +1,11 @@
import React, { useState } from 'react';
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, Play, Pause, X, Save, Building2 } from 'lucide-react';
import { Plus, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play } 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,
usePlanRequirements,
useGenerateProcurementPlan,
useUpdateProcurementPlanStatus,
@@ -16,7 +14,6 @@ import {
import { useTenantStore } from '../../../../stores/tenant.store';
const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('plans');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
@@ -36,17 +33,35 @@ const ProcurementPage: React.FC = () => {
limit: 50,
offset: 0
});
const { data: currentPlan, isLoading: isCurrentPlanLoading } = useCurrentProcurementPlan(tenantId);
const { data: criticalRequirements, isLoading: isCriticalLoading } = useCriticalRequirements(tenantId);
// Get plan requirements for selected plan
const { data: planRequirements, isLoading: isPlanRequirementsLoading } = usePlanRequirements({
const { data: allPlanRequirements, isLoading: isPlanRequirementsLoading } = usePlanRequirements({
tenant_id: tenantId,
plan_id: selectedPlanForRequirements || '',
status: 'critical' // Only get critical requirements
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;
});
// Debug logging
console.log('📊 Plan Requirements Debug:', {
selectedPlanId: selectedPlanForRequirements,
allRequirements: allPlanRequirements?.length || 0,
criticalRequirements: planRequirements?.length || 0,
sampleRequirement: allPlanRequirements?.[0]
});
const generatePlanMutation = useGenerateProcurementPlan();
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
@@ -120,6 +135,7 @@ const ProcurementPage: React.FC = () => {
};
const handleShowCriticalRequirements = (planId: string) => {
console.log('🔍 Opening critical requirements for plan:', planId);
setSelectedPlanForRequirements(planId);
setShowCriticalRequirements(true);
};
@@ -287,70 +303,32 @@ const ProcurementPage: React.FC = () => {
/>
)}
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('plans')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'plans'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Planes de Compra
</button>
<button
onClick={() => setActiveTab('requirements')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'requirements'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Requerimientos Críticos
</button>
<button
onClick={() => setActiveTab('analytics')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'analytics'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Análisis
</button>
</nav>
</div>
{activeTab === 'plans' && (
<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>
<Button variant="outline" onClick={() => console.log('Export filtered')}>
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
<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>
</Card>
)}
<Button variant="outline" onClick={() => console.log('Export filtered')}>
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</Card>
{/* Procurement Plans Grid - Mobile-Optimized */}
{activeTab === 'plans' && (
<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) => {
<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;
@@ -522,12 +500,12 @@ const ProcurementPage: React.FC = () => {
</div>
);
})
)}
</div>
)}
)
}
</div>
{/* Empty State for Procurement Plans */}
{activeTab === 'plans' && !isPlansLoading && filteredPlans.length === 0 && (
{!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">
@@ -558,152 +536,123 @@ const ProcurementPage: React.FC = () => {
</div>
)}
{activeTab === 'requirements' && (
<div className="space-y-4">
{isCriticalLoading ? (
<div className="flex justify-center items-center h-32">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
</div>
) : criticalRequirements && criticalRequirements.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{criticalRequirements.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'}`}
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))}`
]}
actions={[
{
label: 'Ver Detalles',
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => console.log('View requirement details')
},
{
label: 'Asignar Proveedor',
icon: Building2,
priority: 'secondary',
onClick: () => console.log('Assign supplier')
},
{
label: 'Comprar Ahora',
icon: ShoppingCart,
priority: 'secondary',
onClick: () => console.log('Purchase now')
}
]}
/>
))}
</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)]">
Todos los requerimientos están bajo control
</p>
</div>
)}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Costos de Procurement</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Costo Estimado Total</span>
<span className="text-lg font-semibold text-[var(--text-primary)]">
{formatters.currency(stats.totalEstimatedCost)}
</span>
{/* Critical Requirements Modal */}
{showCriticalRequirements && selectedPlanForRequirements && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<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 className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Costo Aprobado</span>
<span className="text-lg font-semibold text-green-600">
{formatters.currency(stats.totalApprovedCost)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-[var(--text-secondary)]">Varianza</span>
<span className="text-lg font-semibold text-[var(--text-primary)]">
{formatters.currency(stats.totalEstimatedCost - stats.totalApprovedCost)}
</span>
<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>
</Card>
<Button
variant="outline"
onClick={handleCloseCriticalRequirements}
className="p-2"
>
<X className="w-4 h-4" />
</Button>
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Críticas</h3>
<div className="space-y-3">
{dashboardData?.low_stock_alerts?.slice(0, 5).map((alert: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-red-50 rounded-lg">
<div className="flex items-center">
<AlertCircle className="w-4 h-4 text-red-500 mr-2" />
<span className="text-sm text-[var(--text-primary)]">{alert.product_name || `Alerta ${index + 1}`}</span>
</div>
<span className="text-xs text-red-600 font-medium">
Stock Bajo
</span>
</div>
)) || (
<div className="text-center py-8">
<CheckCircle className="mx-auto h-8 w-8 text-green-500 mb-2" />
<p className="text-sm text-[var(--text-secondary)]">No hay alertas críticas</p>
</div>
)}
</div>
</Card>
{/* 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="grid gap-4">
{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: () => console.log('View requirement details', requirement)
},
...(requirement.purchase_order_number ? [
{
label: 'Ver PO',
icon: Eye,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => console.log('View PO', requirement.purchase_order_number)
}
] : [
{
label: 'Crear PO',
icon: Plus,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => console.log('Create PO for', requirement)
}
]),
{
label: requirement.supplier_name ? 'Cambiar Proveedor' : 'Asignar Proveedor',
icon: Building2,
variant: 'outline' as const,
priority: 'secondary' as const,
onClick: () => console.log('Assign supplier')
}
]}
/>
))}
</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>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Resumen de Performance</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.totalPlans}</p>
<p className="text-sm text-[var(--text-secondary)] mt-1">Planes Totales</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-green-600">{stats.activePlans}</p>
<p className="text-sm text-[var(--text-secondary)] mt-1">Planes Activos</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-yellow-600">{stats.pendingRequirements}</p>
<p className="text-sm text-[var(--text-secondary)] mt-1">Pendientes</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-red-600">{stats.criticalRequirements}</p>
<p className="text-sm text-[var(--text-secondary)] mt-1">Críticos</p>
</div>
</div>
</Card>
</div>
)}