2025-08-28 10:41:04 +02:00
|
|
|
import React, { useState } from 'react';
|
2025-09-09 12:02:41 +02:00
|
|
|
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader } from 'lucide-react';
|
|
|
|
|
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
2025-08-30 19:11:15 +02:00
|
|
|
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { PageHeader } from '../../../../components/layout';
|
2025-09-09 12:02:41 +02:00
|
|
|
import {
|
|
|
|
|
useProcurementDashboard,
|
|
|
|
|
useProcurementPlans,
|
|
|
|
|
useCurrentProcurementPlan,
|
|
|
|
|
useCriticalRequirements,
|
|
|
|
|
useGenerateProcurementPlan,
|
|
|
|
|
useUpdateProcurementPlanStatus,
|
|
|
|
|
useTriggerDailyScheduler
|
|
|
|
|
} from '../../../../api';
|
|
|
|
|
import { useTenantStore } from '../../../../stores/tenant.store';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const ProcurementPage: React.FC = () => {
|
2025-09-09 12:02:41 +02:00
|
|
|
const [activeTab, setActiveTab] = useState('plans');
|
2025-08-28 10:41:04 +02:00
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
2025-08-31 10:46:13 +02:00
|
|
|
const [showForm, setShowForm] = useState(false);
|
|
|
|
|
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
2025-09-09 12:02:41 +02:00
|
|
|
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
|
|
|
|
|
|
|
|
|
const { currentTenant } = useTenantStore();
|
|
|
|
|
const tenantId = currentTenant?.id || '';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
// Real API data hooks
|
|
|
|
|
const { data: dashboardData, isLoading: isDashboardLoading } = useProcurementDashboard(tenantId);
|
|
|
|
|
const { data: procurementPlans, isLoading: isPlansLoading } = useProcurementPlans({
|
|
|
|
|
tenant_id: tenantId,
|
|
|
|
|
limit: 50,
|
|
|
|
|
offset: 0
|
|
|
|
|
});
|
|
|
|
|
const { data: currentPlan, isLoading: isCurrentPlanLoading } = useCurrentProcurementPlan(tenantId);
|
|
|
|
|
const { data: criticalRequirements, isLoading: isCriticalLoading } = useCriticalRequirements(tenantId);
|
|
|
|
|
|
|
|
|
|
const generatePlanMutation = useGenerateProcurementPlan();
|
|
|
|
|
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
|
|
|
|
|
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
|
|
|
|
|
const getPlanStatusConfig = (status: string) => {
|
2025-08-28 10:41:04 +02:00
|
|
|
const statusConfig = {
|
2025-09-09 12:02:41 +02:00
|
|
|
draft: { text: 'Borrador', icon: Clock },
|
|
|
|
|
pending_approval: { text: 'Pendiente Aprobación', icon: Clock },
|
2025-08-31 10:46:13 +02:00
|
|
|
approved: { text: 'Aprobado', icon: CheckCircle },
|
2025-09-09 12:02:41 +02:00
|
|
|
in_execution: { text: 'En Ejecución', icon: Truck },
|
|
|
|
|
completed: { text: 'Completado', icon: CheckCircle },
|
2025-08-31 10:46:13 +02:00
|
|
|
cancelled: { text: 'Cancelado', icon: AlertCircle },
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const config = statusConfig[status as keyof typeof statusConfig];
|
2025-08-30 19:11:15 +02:00
|
|
|
const Icon = config?.icon;
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-08-31 10:46:13 +02:00
|
|
|
return {
|
2025-09-09 12:02:41 +02:00
|
|
|
color: getStatusColor(status === 'in_execution' ? 'inTransit' : status === 'pending_approval' ? 'pending' : status),
|
2025-08-31 10:46:13 +02:00
|
|
|
text: config?.text || status,
|
|
|
|
|
icon: Icon,
|
2025-09-09 12:02:41 +02:00
|
|
|
isCritical: status === 'cancelled',
|
|
|
|
|
isHighlight: status === 'pending_approval'
|
2025-08-31 10:46:13 +02:00
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
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()));
|
2025-08-30 19:11:15 +02:00
|
|
|
|
|
|
|
|
return matchesSearch;
|
2025-09-09 12:02:41 +02:00
|
|
|
}) || [];
|
2025-08-30 19:11:15 +02:00
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
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,
|
2025-08-30 19:11:15 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
const procurementStats = [
|
2025-08-28 18:07:16 +02:00
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Planes Totales',
|
|
|
|
|
value: stats.totalPlans,
|
2025-08-30 19:11:15 +02:00
|
|
|
variant: 'default' as const,
|
2025-09-09 12:02:41 +02:00
|
|
|
icon: Package,
|
2025-08-28 18:07:16 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Planes Activos',
|
|
|
|
|
value: stats.activePlans,
|
|
|
|
|
variant: 'success' as const,
|
|
|
|
|
icon: CheckCircle,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Requerimientos Pendientes',
|
|
|
|
|
value: stats.pendingRequirements,
|
2025-08-30 19:11:15 +02:00
|
|
|
variant: 'warning' as const,
|
|
|
|
|
icon: Clock,
|
2025-08-28 18:07:16 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Críticos',
|
|
|
|
|
value: stats.criticalRequirements,
|
|
|
|
|
variant: 'warning' as const,
|
|
|
|
|
icon: AlertCircle,
|
2025-08-28 18:07:16 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Costo Estimado',
|
|
|
|
|
value: formatters.currency(stats.totalEstimatedCost),
|
|
|
|
|
variant: 'info' as const,
|
|
|
|
|
icon: DollarSign,
|
2025-08-28 18:07:16 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Costo Aprobado',
|
|
|
|
|
value: formatters.currency(stats.totalApprovedCost),
|
2025-08-30 19:11:15 +02:00
|
|
|
variant: 'success' as const,
|
|
|
|
|
icon: DollarSign,
|
2025-08-28 18:07:16 +02:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
return (
|
2025-08-30 19:21:15 +02:00
|
|
|
<div className="space-y-6">
|
2025-08-28 10:41:04 +02:00
|
|
|
<PageHeader
|
2025-09-09 12:02:41 +02:00
|
|
|
title="Planificación de Compras"
|
|
|
|
|
description="Administra planes de compras, requerimientos y análisis de procurement"
|
2025-08-30 19:11:15 +02:00
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
id: "export",
|
|
|
|
|
label: "Exportar",
|
|
|
|
|
variant: "outline" as const,
|
|
|
|
|
icon: Download,
|
2025-09-09 12:02:41 +02:00
|
|
|
onClick: () => console.log('Export procurement data')
|
2025-08-30 19:11:15 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
id: "generate",
|
|
|
|
|
label: "Generar Plan",
|
2025-08-30 19:11:15 +02:00
|
|
|
variant: "primary" as const,
|
|
|
|
|
icon: Plus,
|
2025-09-09 12:02:41 +02:00
|
|
|
onClick: () => generatePlanMutation.mutate({
|
|
|
|
|
tenantId,
|
|
|
|
|
request: {
|
|
|
|
|
force_regenerate: false,
|
|
|
|
|
planning_horizon_days: 14,
|
|
|
|
|
include_safety_stock: true,
|
|
|
|
|
safety_stock_percentage: 20
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "trigger",
|
|
|
|
|
label: "Ejecutar Programador",
|
|
|
|
|
variant: "outline" as const,
|
|
|
|
|
icon: Calendar,
|
|
|
|
|
onClick: () => triggerSchedulerMutation.mutate(tenantId)
|
2025-08-30 19:11:15 +02:00
|
|
|
}
|
|
|
|
|
]}
|
2025-08-28 10:41:04 +02:00
|
|
|
/>
|
|
|
|
|
|
2025-08-30 19:11:15 +02:00
|
|
|
{/* Stats Grid */}
|
2025-09-09 12:02:41 +02:00
|
|
|
{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}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
{/* Tabs Navigation */}
|
|
|
|
|
<div className="border-b border-[var(--border-primary)]">
|
|
|
|
|
<nav className="-mb-px flex space-x-8">
|
|
|
|
|
<button
|
2025-09-09 12:02:41 +02:00
|
|
|
onClick={() => setActiveTab('plans')}
|
2025-08-28 10:41:04 +02:00
|
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
2025-09-09 12:02:41 +02:00
|
|
|
activeTab === 'plans'
|
2025-08-28 10:41:04 +02:00
|
|
|
? 'border-orange-500 text-[var(--color-primary)]'
|
|
|
|
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2025-09-09 12:02:41 +02:00
|
|
|
Planes de Compra
|
2025-08-28 10:41:04 +02:00
|
|
|
</button>
|
|
|
|
|
<button
|
2025-09-09 12:02:41 +02:00
|
|
|
onClick={() => setActiveTab('requirements')}
|
2025-08-28 10:41:04 +02:00
|
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
2025-09-09 12:02:41 +02:00
|
|
|
activeTab === 'requirements'
|
2025-08-28 10:41:04 +02:00
|
|
|
? 'border-orange-500 text-[var(--color-primary)]'
|
|
|
|
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2025-09-09 12:02:41 +02:00
|
|
|
Requerimientos Críticos
|
2025-08-28 10:41:04 +02:00
|
|
|
</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>
|
|
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
{activeTab === 'plans' && (
|
2025-08-30 19:11:15 +02:00
|
|
|
<Card className="p-4">
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
|
|
|
<div className="flex-1">
|
2025-08-28 10:41:04 +02:00
|
|
|
<Input
|
2025-09-09 12:02:41 +02:00
|
|
|
placeholder="Buscar planes por número, estado o notas..."
|
2025-08-28 10:41:04 +02:00
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
2025-08-30 19:11:15 +02:00
|
|
|
className="w-full"
|
2025-08-28 10:41:04 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
<Button variant="outline" onClick={() => console.log('Export filtered')}>
|
2025-08-28 10:41:04 +02:00
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
Exportar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
{/* Procurement Plans Grid */}
|
|
|
|
|
{activeTab === 'plans' && (
|
2025-08-30 19:11:15 +02:00
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
2025-09-09 12:02:41 +02:00
|
|
|
{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);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StatusCard
|
|
|
|
|
key={plan.id}
|
|
|
|
|
id={plan.plan_number}
|
|
|
|
|
statusIndicator={statusConfig}
|
|
|
|
|
title={`Plan ${plan.plan_number}`}
|
|
|
|
|
subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')}
|
|
|
|
|
primaryValue={formatters.currency(plan.total_estimated_cost)}
|
|
|
|
|
primaryValueLabel={`${plan.total_requirements} requerimientos`}
|
|
|
|
|
secondaryInfo={{
|
|
|
|
|
label: 'Período',
|
|
|
|
|
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
|
|
|
|
|
}}
|
|
|
|
|
metadata={[
|
|
|
|
|
`${plan.planning_horizon_days} días de horizonte`,
|
|
|
|
|
`Estrategia: ${plan.procurement_strategy}`,
|
|
|
|
|
...(plan.special_requirements ? [`"${plan.special_requirements}"`] : [])
|
|
|
|
|
]}
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
label: 'Ver',
|
|
|
|
|
icon: Eye,
|
|
|
|
|
variant: 'outline',
|
|
|
|
|
onClick: () => {
|
|
|
|
|
setSelectedPlan(plan);
|
|
|
|
|
setModalMode('view');
|
|
|
|
|
setShowForm(true);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
...(plan.status === 'pending_approval' ? [{
|
|
|
|
|
label: 'Aprobar',
|
|
|
|
|
icon: CheckCircle,
|
|
|
|
|
variant: 'outline' as const,
|
|
|
|
|
onClick: () => {
|
|
|
|
|
updatePlanStatusMutation.mutate({
|
|
|
|
|
tenant_id: tenantId,
|
|
|
|
|
plan_id: plan.id,
|
|
|
|
|
status: 'approved'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}] : [])
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
)}
|
2025-08-30 19:11:15 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
{/* Empty State for Procurement Plans */}
|
|
|
|
|
{activeTab === 'plans' && !isPlansLoading && filteredPlans.length === 0 && (
|
2025-08-30 19:11:15 +02:00
|
|
|
<div className="text-center py-12">
|
2025-09-09 12:02:41 +02:00
|
|
|
<Package className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
2025-08-30 19:11:15 +02:00
|
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
2025-09-09 12:02:41 +02:00
|
|
|
No se encontraron planes de compra
|
2025-08-30 19:11:15 +02:00
|
|
|
</h3>
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
2025-09-09 12:02:41 +02:00
|
|
|
Intenta ajustar la búsqueda o generar un nuevo plan de compra
|
2025-08-30 19:11:15 +02:00
|
|
|
</p>
|
2025-09-09 12:02:41 +02:00
|
|
|
<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
|
2025-08-30 19:11:15 +02:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
{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}
|
|
|
|
|
primaryValue={`${requirement.required_quantity} ${requirement.unit_of_measure}`}
|
|
|
|
|
primaryValueLabel="Cantidad requerida"
|
|
|
|
|
secondaryInfo={{
|
|
|
|
|
label: 'Fecha límite',
|
|
|
|
|
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES')
|
|
|
|
|
}}
|
|
|
|
|
metadata={[
|
|
|
|
|
`Stock actual: ${requirement.current_stock_level} ${requirement.unit_of_measure}`,
|
|
|
|
|
`Proveedor: ${requirement.supplier_name || 'No asignado'}`,
|
|
|
|
|
`Costo estimado: ${formatters.currency(requirement.estimated_total_cost || 0)}`
|
|
|
|
|
]}
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
label: 'Ver Detalles',
|
|
|
|
|
icon: Eye,
|
|
|
|
|
variant: 'outline',
|
|
|
|
|
onClick: () => console.log('View requirement details')
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</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>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'analytics' && (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
<Card className="p-6">
|
2025-09-09 12:02:41 +02:00
|
|
|
<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>
|
|
|
|
|
</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>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="p-6">
|
2025-09-09 12:02:41 +02:00
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Críticas</h3>
|
2025-08-28 10:41:04 +02:00
|
|
|
<div className="space-y-3">
|
2025-09-09 12:02:41 +02:00
|
|
|
{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>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-09-09 12:02:41 +02:00
|
|
|
<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>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card className="p-6">
|
2025-09-09 12:02:41 +02:00
|
|
|
<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>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-08-31 10:46:13 +02:00
|
|
|
|
2025-09-09 12:02:41 +02:00
|
|
|
{/* Procurement Plan Modal */}
|
|
|
|
|
{showForm && selectedPlan && (
|
2025-08-31 10:46:13 +02:00
|
|
|
<StatusModal
|
|
|
|
|
isOpen={showForm}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowForm(false);
|
2025-09-09 12:02:41 +02:00
|
|
|
setSelectedPlan(null);
|
2025-08-31 10:46:13 +02:00
|
|
|
setModalMode('view');
|
|
|
|
|
}}
|
|
|
|
|
mode={modalMode}
|
|
|
|
|
onModeChange={setModalMode}
|
2025-09-09 12:02:41 +02:00
|
|
|
title={`Plan de Compra ${selectedPlan.plan_number}`}
|
|
|
|
|
subtitle={new Date(selectedPlan.plan_date).toLocaleDateString('es-ES')}
|
|
|
|
|
statusIndicator={getPlanStatusConfig(selectedPlan.status)}
|
2025-08-31 10:46:13 +02:00
|
|
|
size="lg"
|
|
|
|
|
sections={[
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Información del Plan',
|
2025-08-31 10:46:13 +02:00
|
|
|
icon: Package,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Número de Plan',
|
|
|
|
|
value: selectedPlan.plan_number,
|
|
|
|
|
highlight: true
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Tipo de Plan',
|
|
|
|
|
value: selectedPlan.plan_type
|
2025-08-31 10:46:13 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Estrategia',
|
|
|
|
|
value: selectedPlan.procurement_strategy
|
2025-08-31 10:46:13 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Prioridad',
|
|
|
|
|
value: selectedPlan.priority,
|
2025-08-31 10:46:13 +02:00
|
|
|
type: 'status'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Fechas y Períodos',
|
2025-08-31 10:46:13 +02:00
|
|
|
icon: Calendar,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Fecha del Plan',
|
|
|
|
|
value: selectedPlan.plan_date,
|
|
|
|
|
type: 'date'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Período de Inicio',
|
|
|
|
|
value: selectedPlan.plan_period_start,
|
|
|
|
|
type: 'date'
|
2025-08-31 10:46:13 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Período de Fin',
|
|
|
|
|
value: selectedPlan.plan_period_end,
|
2025-08-31 10:46:13 +02:00
|
|
|
type: 'date',
|
2025-09-09 12:02:41 +02:00
|
|
|
highlight: true
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Horizonte de Planificación',
|
|
|
|
|
value: `${selectedPlan.planning_horizon_days} días`
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Información Financiera',
|
|
|
|
|
icon: DollarSign,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Costo Estimado Total',
|
|
|
|
|
value: selectedPlan.total_estimated_cost,
|
2025-08-31 10:46:13 +02:00
|
|
|
type: 'currency',
|
2025-09-09 12:02:41 +02:00
|
|
|
highlight: true
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Costo Aprobado Total',
|
|
|
|
|
value: selectedPlan.total_approved_cost,
|
|
|
|
|
type: 'currency'
|
2025-08-31 10:46:13 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
label: 'Varianza de Costo',
|
|
|
|
|
value: selectedPlan.cost_variance,
|
|
|
|
|
type: 'currency'
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
title: 'Estadísticas',
|
2025-08-31 10:46:13 +02:00
|
|
|
icon: ShoppingCart,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2025-09-09 12:02:41 +02:00
|
|
|
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`
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
2025-09-09 12:02:41 +02:00
|
|
|
...(selectedPlan.special_requirements ? [{
|
|
|
|
|
title: 'Requerimientos Especiales',
|
2025-08-31 10:46:13 +02:00
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
label: 'Observaciones',
|
2025-09-09 12:02:41 +02:00
|
|
|
value: selectedPlan.special_requirements,
|
2025-08-31 10:46:13 +02:00
|
|
|
span: 2 as const,
|
|
|
|
|
editable: true,
|
2025-09-09 12:02:41 +02:00
|
|
|
placeholder: 'Añadir requerimientos especiales para el plan...'
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}] : [])
|
|
|
|
|
]}
|
|
|
|
|
onEdit={() => {
|
2025-09-09 12:02:41 +02:00
|
|
|
console.log('Editing procurement plan:', selectedPlan.id);
|
2025-08-31 10:46:13 +02:00
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ProcurementPage;
|