Files
bakery-ia/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx
2025-11-02 20:24:44 +01:00

474 lines
21 KiB
TypeScript

import React, { useState } from 'react';
import {
ShoppingCart,
TrendingUp,
AlertCircle,
Target,
DollarSign,
Award,
BarChart3,
Truck,
Calendar
} from 'lucide-react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend
} from 'recharts';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../components/analytics';
import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProcurementDashboard, useProcurementTrends } from '../../../api/hooks/procurement';
import { formatters } from '../../../components/ui/Stats/StatsPresets';
const ProcurementAnalyticsPage: React.FC = () => {
const { canAccessAnalytics, subscriptionInfo } = useSubscription();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const [activeTab, setActiveTab] = useState('overview');
const { data: dashboard, isLoading: dashboardLoading } = useProcurementDashboard(tenantId);
const { data: trends, isLoading: trendsLoading } = useProcurementTrends(tenantId, 7);
// Check if user has access to advanced analytics (professional/enterprise)
const hasAdvancedAccess = canAccessAnalytics('advanced');
// 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 (
<AnalyticsPageLayout
title="Analítica de Compras"
description="Insights avanzados sobre planificación de compras y gestión de proveedores"
subscriptionLoading={subscriptionInfo.loading}
hasAccess={hasAdvancedAccess}
dataLoading={dashboardLoading}
stats={[
{
title: 'Planes Activos',
value: dashboard?.summary?.total_plans || 0,
icon: ShoppingCart,
formatter: formatters.number
},
{
title: 'Tasa de Cumplimiento',
value: dashboard?.performance_metrics?.average_fulfillment_rate || 0,
icon: Target,
formatter: formatters.percentage
},
{
title: 'Entregas a Tiempo',
value: dashboard?.performance_metrics?.average_on_time_delivery || 0,
icon: Calendar,
formatter: formatters.percentage
},
{
title: 'Variación de Costos',
value: dashboard?.performance_metrics?.cost_accuracy || 0,
icon: DollarSign,
formatter: formatters.percentage
}
]}
statsColumns={4}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
showMobileNotice={true}
>
{activeTab === 'overview' && (
<>
{/* Overview Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Plan Status Distribution */}
<AnalyticsCard title="Distribución de Estados de Planes">
<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?.summary?.total_plans || 1)) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-8 text-right">
{status.count}
</span>
</div>
</div>
))}
</div>
</AnalyticsCard>
{/* Critical Requirements */}
<AnalyticsCard
title="Requerimientos Críticos"
actions={<AlertCircle className="h-5 w-5 text-[var(--color-error)]" />}
>
<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>
</AnalyticsCard>
</div>
{/* Recent Plans */}
<AnalyticsCard title="Planes Recientes">
<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>
</AnalyticsCard>
</>
)}
{activeTab === 'performance' && (
<>
{/* Performance Tab */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<AnalyticsCard>
<div className="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?.performance_metrics?.average_fulfillment_rate || 0)}
</div>
<div className="text-sm text-[var(--text-secondary)]">Tasa de Cumplimiento</div>
</div>
</AnalyticsCard>
<AnalyticsCard>
<div className="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?.performance_metrics?.average_on_time_delivery || 0)}
</div>
<div className="text-sm text-[var(--text-secondary)]">Entregas a Tiempo</div>
</div>
</AnalyticsCard>
<AnalyticsCard>
<div className="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?.performance_metrics?.supplier_performance?.toFixed(1) || '0.0'}
</div>
<div className="text-sm text-[var(--text-secondary)]">Puntuación de Calidad</div>
</div>
</AnalyticsCard>
</div>
{/* Performance Trend Chart */}
<AnalyticsCard title="Tendencias de Rendimiento (Últimos 7 días)" loading={trendsLoading}>
{trends && trends.performance_trend && trends.performance_trend.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trends.performance_trend}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
<XAxis
dataKey="date"
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
/>
<YAxis
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
tickFormatter={(value) => `${(value * 100).toFixed(0)}%`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--bg-primary)',
border: '1px solid var(--border-primary)',
borderRadius: '8px'
}}
formatter={(value: any) => `${(value * 100).toFixed(1)}%`}
labelStyle={{ color: 'var(--text-primary)' }}
/>
<Legend />
<Line
type="monotone"
dataKey="fulfillment_rate"
stroke="var(--color-success)"
strokeWidth={2}
name="Tasa de Cumplimiento"
dot={{ fill: 'var(--color-success)' }}
/>
<Line
type="monotone"
dataKey="on_time_rate"
stroke="var(--color-info)"
strokeWidth={2}
name="Entregas a Tiempo"
dot={{ fill: 'var(--color-info)' }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-[var(--text-tertiary)]">
No hay datos de tendencias disponibles
</div>
)}
</AnalyticsCard>
</>
)}
{activeTab === 'suppliers' && (
<>
{/* Suppliers Tab */}
<AnalyticsCard title="Rendimiento de Proveedores">
<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>
</AnalyticsCard>
</>
)}
{activeTab === 'costs' && (
<>
{/* Costs Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<AnalyticsCard title="Análisis de Costos">
<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?.summary?.total_estimated_cost || 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?.summary?.total_approved_cost || 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?.summary?.cost_variance || 0) > 0
? 'text-[var(--color-error)]'
: 'text-[var(--color-success)]'
}`}>
{formatters.currency(Math.abs(dashboard?.summary?.cost_variance || 0))}
</span>
</div>
</div>
</AnalyticsCard>
<AnalyticsCard title="Distribución de Costos por Categoría">
<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?.summary?.total_estimated_cost || 1)) * 100}%` }}
/>
</div>
<span className="text-sm font-medium text-[var(--text-primary)] w-20 text-right">
{formatters.currency(category.amount)}
</span>
</div>
</div>
))}
</div>
</AnalyticsCard>
</div>
</>
)}
{activeTab === 'quality' && (
<>
{/* Quality Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<AnalyticsCard title="Métricas de Calidad">
<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>
</AnalyticsCard>
<AnalyticsCard title="Tendencia de Calidad (Últimos 7 días)" loading={trendsLoading}>
{trends && trends.quality_trend && trends.quality_trend.length > 0 ? (
<ResponsiveContainer width="100%" height={200}>
<LineChart data={trends.quality_trend}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
<XAxis
dataKey="date"
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
/>
<YAxis
stroke="var(--text-tertiary)"
tick={{ fill: 'var(--text-secondary)' }}
domain={[0, 10]}
ticks={[0, 2, 4, 6, 8, 10]}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--bg-primary)',
border: '1px solid var(--border-primary)',
borderRadius: '8px'
}}
formatter={(value: any) => `${value.toFixed(1)} / 10`}
labelStyle={{ color: 'var(--text-primary)' }}
/>
<Line
type="monotone"
dataKey="quality_score"
stroke="var(--color-warning)"
strokeWidth={2}
name="Puntuación de Calidad"
dot={{ fill: 'var(--color-warning)' }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-48 flex items-center justify-center text-[var(--text-tertiary)]">
No hay datos de calidad disponibles
</div>
)}
</AnalyticsCard>
</div>
</>
)}
</AnalyticsPageLayout>
);
};
// 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;