Imporve the AI models page
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user