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