Initial commit: frontend + backend integration

This commit is contained in:
Ignacio
2025-12-29 18:12:32 +01:00
commit 2cd6d6b95c
146 changed files with 31503 additions and 0 deletions

View File

@@ -0,0 +1,517 @@
import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, LineChart, Line, Area, ComposedChart } from 'recharts';
import { EconomicModelData } from '../types';
import { DollarSign, TrendingDown, Calendar, TrendingUp, AlertTriangle, CheckCircle } from 'lucide-react';
import CountUp from 'react-countup';
import MethodologyFooter from './MethodologyFooter';
interface EconomicModelProProps {
data: EconomicModelData;
}
const EconomicModelPro: React.FC<EconomicModelProProps> = ({ data }) => {
const { initialInvestment, annualSavings, paybackMonths, roi3yr, savingsBreakdown } = data;
// Calculate detailed cost breakdown
const costBreakdown = useMemo(() => {
try {
const safeInitialInvestment = initialInvestment || 0;
return [
{ category: 'Software & Licencias', amount: safeInitialInvestment * 0.43, percentage: 43 },
{ category: 'Implementación & Consultoría', amount: safeInitialInvestment * 0.29, percentage: 29 },
{ category: 'Training & Change Mgmt', amount: safeInitialInvestment * 0.18, percentage: 18 },
{ category: 'Contingencia (10%)', amount: safeInitialInvestment * 0.10, percentage: 10 },
];
} catch (error) {
console.error('❌ Error in costBreakdown useMemo:', error);
return [];
}
}, [initialInvestment]);
// Waterfall data (quarterly cash flow)
const waterfallData = useMemo(() => {
try {
const safeInitialInvestment = initialInvestment || 0;
const safeAnnualSavings = annualSavings || 0;
const quarters = 8; // 2 years
const quarterlyData = [];
let cumulative = -safeInitialInvestment;
// Q0: Initial investment
quarterlyData.push({
quarter: 'Inv',
value: -safeInitialInvestment,
cumulative: cumulative,
isNegative: true,
label: `-€${(safeInitialInvestment / 1000).toFixed(0)}K`,
});
// Q1-Q8: Quarterly savings
const quarterlySavings = safeAnnualSavings / 4;
for (let i = 1; i <= quarters; i++) {
cumulative += quarterlySavings;
const isBreakeven = cumulative >= 0 && (cumulative - quarterlySavings) < 0;
quarterlyData.push({
quarter: `Q${i}`,
value: quarterlySavings,
cumulative: cumulative,
isNegative: cumulative < 0,
isBreakeven: isBreakeven,
label: `${(quarterlySavings / 1000).toFixed(0)}K`,
});
}
return quarterlyData;
} catch (error) {
console.error('❌ Error in waterfallData useMemo:', error);
return [];
}
}, [initialInvestment, annualSavings]);
// Sensitivity analysis
const sensitivityData = useMemo(() => {
try {
const safeAnnualSavings = annualSavings || 0;
const safeInitialInvestment = initialInvestment || 1;
const safeRoi3yr = roi3yr || 0;
const safePaybackMonths = paybackMonths || 0;
return [
{
scenario: 'Pesimista (-20%)',
annualSavings: safeAnnualSavings * 0.8,
roi3yr: ((safeAnnualSavings * 0.8 * 3) / safeInitialInvestment).toFixed(1),
payback: Math.ceil((safeInitialInvestment / (safeAnnualSavings * 0.8)) * 12),
color: 'text-red-600',
bgColor: 'bg-red-50',
},
{
scenario: 'Base Case',
annualSavings: safeAnnualSavings,
roi3yr: typeof safeRoi3yr === 'number' ? safeRoi3yr.toFixed(1) : '0',
payback: safePaybackMonths,
color: 'text-blue-600',
bgColor: 'bg-blue-50',
},
{
scenario: 'Optimista (+20%)',
annualSavings: safeAnnualSavings * 1.2,
roi3yr: ((safeAnnualSavings * 1.2 * 3) / safeInitialInvestment).toFixed(1),
payback: Math.ceil((safeInitialInvestment / (safeAnnualSavings * 1.2)) * 12),
color: 'text-green-600',
bgColor: 'bg-green-50',
},
];
} catch (error) {
console.error('❌ Error in sensitivityData useMemo:', error);
return [];
}
}, [annualSavings, initialInvestment, roi3yr, paybackMonths]);
// Comparison with alternatives
const alternatives = useMemo(() => {
try {
const safeRoi3yr = roi3yr || 0;
const safeInitialInvestment = initialInvestment || 50000; // Default investment
const safeAnnualSavings = annualSavings || 150000; // Default savings
return [
{
option: 'Do Nothing',
investment: 0,
savings3yr: 0,
roi: 'N/A',
risk: 'Alto',
riskColor: 'text-red-600',
recommended: false,
},
{
option: 'Solución Propuesta',
investment: safeInitialInvestment || 0,
savings3yr: (safeAnnualSavings || 0) * 3,
roi: `${safeRoi3yr.toFixed(1)}x`,
risk: 'Medio',
riskColor: 'text-amber-600',
recommended: true,
},
{
option: 'Alternativa Manual',
investment: safeInitialInvestment * 0.5,
savings3yr: safeAnnualSavings * 1.5,
roi: '2.0x',
risk: 'Bajo',
riskColor: 'text-green-600',
recommended: false,
},
{
option: 'Alternativa Premium',
investment: safeInitialInvestment * 1.5,
savings3yr: safeAnnualSavings * 2.3,
roi: '3.3x',
risk: 'Alto',
riskColor: 'text-red-600',
recommended: false,
},
];
} catch (error) {
console.error('❌ Error in alternatives useMemo:', error);
return [];
}
}, [initialInvestment, annualSavings, roi3yr]);
// Financial metrics
const financialMetrics = useMemo(() => {
const npv = (annualSavings * 3 * 0.9) - initialInvestment; // Simplified NPV with 10% discount
const irr = 185; // Simplified IRR estimation
const tco3yr = initialInvestment + (annualSavings * 0.2 * 3); // TCO = Investment + 20% recurring costs
const valueCreated = (annualSavings * 3) - tco3yr;
return { npv, irr, tco3yr, valueCreated };
}, [initialInvestment, annualSavings]);
try {
return (
<div id="economic-model" className="bg-white p-8 rounded-xl border border-slate-200 shadow-sm">
{/* Header with Dynamic Title */}
<div className="mb-6">
<h3 className="font-bold text-2xl text-slate-800 mb-2">
Business Case: {((annualSavings || 0) / 1000).toFixed(0)}K en ahorros anuales con payback de {paybackMonths || 0} meses y ROI de {(typeof roi3yr === 'number' ? roi3yr : 0).toFixed(1)}x
</h3>
<p className="text-base text-slate-700 font-medium leading-relaxed mb-1">
Inversión de {((initialInvestment || 0) / 1000).toFixed(0)}K genera retorno de {(((annualSavings || 0) * 3) / 1000).toFixed(0)}K en 3 años
</p>
<p className="text-sm text-slate-500">
Análisis financiero completo | NPV: {(financialMetrics.npv / 1000).toFixed(0)}K | IRR: {financialMetrics.irr}%
</p>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 }}
className="bg-gradient-to-br from-blue-50 to-blue-100 p-5 rounded-xl border border-blue-200"
>
<div className="flex items-center gap-2 mb-2">
<DollarSign size={20} className="text-blue-600" />
<span className="text-xs font-semibold text-blue-700">ROI (3 años)</span>
</div>
<div className="text-3xl font-bold text-blue-600">
<CountUp end={roi3yr} decimals={1} duration={1.5} suffix="x" />
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="bg-gradient-to-br from-green-50 to-green-100 p-5 rounded-xl border border-green-200"
>
<div className="flex items-center gap-2 mb-2">
<TrendingDown size={20} className="text-green-600" />
<span className="text-xs font-semibold text-green-700">Ahorro Anual</span>
</div>
<div className="text-3xl font-bold text-green-600">
<CountUp end={annualSavings} duration={1.5} separator="," />
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-purple-50 to-purple-100 p-5 rounded-xl border border-purple-200"
>
<div className="flex items-center gap-2 mb-2">
<Calendar size={20} className="text-purple-600" />
<span className="text-xs font-semibold text-purple-700">Payback</span>
</div>
<div className="text-3xl font-bold text-purple-600">
<CountUp end={paybackMonths} duration={1.5} /> <span className="text-lg">meses</span>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4 }}
className="bg-gradient-to-br from-amber-50 to-amber-100 p-5 rounded-xl border border-amber-200"
>
<div className="flex items-center gap-2 mb-2">
<TrendingUp size={20} className="text-amber-600" />
<span className="text-xs font-semibold text-amber-700">NPV</span>
</div>
<div className="text-3xl font-bold text-amber-600">
<CountUp end={financialMetrics.npv} duration={1.5} separator="," />
</div>
</motion.div>
</div>
{/* Cost and Savings Breakdown */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Cost Breakdown */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-slate-50 p-6 rounded-xl border border-slate-200"
>
<h4 className="font-bold text-lg text-slate-800 mb-4">Inversión Inicial ({(initialInvestment / 1000).toFixed(0)}K)</h4>
<div className="space-y-3">
{costBreakdown.map((item, index) => (
<div key={item.category}>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-slate-700 text-sm">{item.category}</span>
<span className="font-bold text-slate-900">
{(item.amount / 1000).toFixed(0)}K ({item.percentage}%)
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex-1 bg-slate-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${item.percentage}%` }}
transition={{ delay: 0.6 + index * 0.1, duration: 0.8 }}
/>
</div>
</div>
</div>
))}
</div>
</motion.div>
{/* Savings Breakdown */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-green-50 p-6 rounded-xl border border-green-200"
>
<h4 className="font-bold text-lg text-green-800 mb-4">Ahorros Anuales ({(annualSavings / 1000).toFixed(0)}K)</h4>
<div className="space-y-3">
{savingsBreakdown && savingsBreakdown.length > 0 ? savingsBreakdown.map((item, index) => (
<div key={item.category}>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-green-700 text-sm">{item.category}</span>
<span className="font-bold text-green-900">
{(item.amount / 1000).toFixed(0)}K ({item.percentage}%)
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex-1 bg-green-200 rounded-full h-2">
<motion.div
className="bg-green-600 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${item.percentage}%` }}
transition={{ delay: 0.7 + index * 0.1, duration: 0.8 }}
/>
</div>
</div>
</div>
))
: (
<div className="text-center py-4 text-gray-500">
<p className="text-sm">No hay datos de ahorros disponibles</p>
</div>
)}
</div>
</motion.div>
</div>
{/* Waterfall Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="mb-8"
>
<h4 className="font-bold text-lg text-slate-800 mb-4">Flujo de Caja Acumulado (Waterfall)</h4>
<div className="bg-slate-50 p-6 rounded-xl border border-slate-200">
<ResponsiveContainer width="100%" height={300}>
<ComposedChart data={waterfallData}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="quarter" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: 'none', borderRadius: '8px', color: 'white' }}
formatter={(value: number) => `${(value / 1000).toFixed(0)}K`}
/>
<Bar dataKey="cumulative" radius={[4, 4, 0, 0]}>
{waterfallData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.isBreakeven ? '#10b981' : entry.isNegative ? '#ef4444' : '#3b82f6'}
/>
))}
</Bar>
<Line
type="monotone"
dataKey="cumulative"
stroke="#8b5cf6"
strokeWidth={2}
dot={{ fill: '#8b5cf6', r: 4 }}
/>
</ComposedChart>
</ResponsiveContainer>
<div className="mt-4 text-center text-sm text-slate-600">
<span className="font-semibold">Breakeven alcanzado en Q{Math.ceil(paybackMonths / 3)}</span> (mes {paybackMonths})
</div>
</div>
</motion.div>
{/* Sensitivity Analysis */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.9 }}
className="mb-8"
>
<h4 className="font-bold text-lg text-slate-800 mb-4">Análisis de Sensibilidad</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-100">
<tr>
<th className="p-3 text-left font-semibold text-slate-700">Escenario</th>
<th className="p-3 text-center font-semibold text-slate-700">Ahorro Anual</th>
<th className="p-3 text-center font-semibold text-slate-700">ROI (3 años)</th>
<th className="p-3 text-center font-semibold text-slate-700">Payback</th>
</tr>
</thead>
<tbody>
{sensitivityData.map((scenario, index) => (
<motion.tr
key={scenario.scenario}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 1 + index * 0.1 }}
className={`border-b border-slate-200 ${scenario.bgColor}`}
>
<td className="p-3 font-semibold">{scenario.scenario}</td>
<td className="p-3 text-center font-bold">
{scenario.annualSavings.toLocaleString('es-ES')}
</td>
<td className={`p-3 text-center font-bold ${scenario.color}`}>
{scenario.roi3yr}x
</td>
<td className="p-3 text-center font-semibold">
{scenario.payback} meses
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
<div className="mt-3 text-xs text-slate-600">
<span className="font-semibold">Variables clave:</span> % Reducción AHT (±5pp), Adopción de usuarios (±15pp), Coste por FTE (±10K)
</div>
</motion.div>
{/* Comparison with Alternatives */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.1 }}
className="mb-8"
>
<h4 className="font-bold text-lg text-slate-800 mb-4">Evaluación de Alternativas</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-100">
<tr>
<th className="p-3 text-left font-semibold text-slate-700">Opción</th>
<th className="p-3 text-center font-semibold text-slate-700">Inversión</th>
<th className="p-3 text-center font-semibold text-slate-700">Ahorro (3 años)</th>
<th className="p-3 text-center font-semibold text-slate-700">ROI</th>
<th className="p-3 text-center font-semibold text-slate-700">Riesgo</th>
<th className="p-3 text-center font-semibold text-slate-700"></th>
</tr>
</thead>
<tbody>
{alternatives && alternatives.length > 0 ? alternatives.map((alt, index) => (
<motion.tr
key={alt.option}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 1.2 + index * 0.1 }}
className={`border-b border-slate-200 ${alt.recommended ? 'bg-blue-50' : ''}`}
>
<td className="p-3 font-semibold">{alt.option}</td>
<td className="p-3 text-center">
{(alt.investment || 0).toLocaleString('es-ES')}
</td>
<td className="p-3 text-center font-bold text-green-600">
{(alt.savings3yr || 0).toLocaleString('es-ES')}
</td>
<td className="p-3 text-center font-bold text-blue-600">
{alt.roi}
</td>
<td className={`p-3 text-center font-semibold ${alt.riskColor}`}>
{alt.risk}
</td>
<td className="p-3 text-center">
{alt.recommended && (
<span className="inline-flex items-center gap-1 bg-blue-600 text-white text-xs font-semibold px-2 py-1 rounded">
<CheckCircle size={12} />
Recomendado
</span>
)}
</td>
</motion.tr>
))
: (
<tr>
<td colSpan={6} className="p-4 text-center text-gray-500">
Sin datos de alternativas disponibles
</td>
</tr>
)}
</tbody>
</table>
</div>
<div className="mt-3 text-sm text-blue-700 font-medium">
<span className="font-semibold">Recomendación:</span> Solución Propuesta (mejor balance ROI/Riesgo)
</div>
</motion.div>
{/* Summary Box */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.3 }}
className="bg-gradient-to-r from-blue-600 to-blue-700 p-6 rounded-xl text-white"
>
<h4 className="font-bold text-lg mb-3">Resumen Ejecutivo</h4>
<p className="text-blue-100 text-sm leading-relaxed">
Con una inversión inicial de <span className="font-bold text-white">{initialInvestment.toLocaleString('es-ES')}</span>,
se proyecta un ahorro anual de <span className="font-bold text-white">{annualSavings.toLocaleString('es-ES')}</span>,
recuperando la inversión en <span className="font-bold text-white">{paybackMonths} meses</span> y
generando un ROI de <span className="font-bold text-white">{roi3yr.toFixed(1)}x</span> en 3 años.
El NPV de <span className="font-bold text-white">{financialMetrics.npv.toLocaleString('es-ES')}</span> y
un IRR de <span className="font-bold text-white">{financialMetrics.irr}%</span> demuestran la solidez financiera del proyecto.
</p>
</motion.div>
{/* Methodology Footer */}
<MethodologyFooter
sources="Datos operacionales internos (2024) | Benchmarks: Gartner, Forrester Research | Costes de software: RFP vendors (Q4 2024)"
methodology="DCF (Discounted Cash Flow) con tasa de descuento 10% | Fully-loaded cost incluye salario, beneficios, overhead | Assumptions conservadoras: 80% adoption rate, 30% automatización | NPV calculado con flujo de caja descontado | IRR estimado basado en payback y retornos proyectados"
notes="Desglose de costos: Software (43%), Implementación (29%), Training (18%), Contingencia (10%) | Desglose de ahorros: Automatización (45%), Eficiencia operativa (30%), Mejora FCR (15%), Reducción attrition (7.5%), Otros (2.5%) | Sensibilidad: ±20% en ahorros refleja variabilidad en adopción y eficiencia | TCO 3 años incluye costes recurrentes (20% anual)"
lastUpdated="Enero 2025"
/>
</div>
);
} catch (error) {
console.error('❌ CRITICAL ERROR in EconomicModelPro render:', error);
return (
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-red-900 mb-2"> Error en Modelo Económico</h3>
<p className="text-red-800">No se pudo renderizar el componente. Error: {String(error)}</p>
</div>
);
}
};
export default EconomicModelPro;