Files
econ/frontend/src/components/exercises/modulo3/UtilidadTotalVsMarginal.tsx
2026-03-31 01:28:28 -03:00

244 lines
10 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface UtilidadTotalVsMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDatos {
cantidad: number;
utilidadTotal: number;
utilidadMarginal: number | null;
}
const datosBase: Omit<FilaDatos, 'utilidadMarginal'>[] = [
{ cantidad: 0, utilidadTotal: 0 },
{ cantidad: 1, utilidadTotal: 10 },
{ cantidad: 2, utilidadTotal: 18 },
{ cantidad: 3, utilidadTotal: 24 },
{ cantidad: 4, utilidadTotal: 28 },
{ cantidad: 5, utilidadTotal: 30 },
{ cantidad: 6, utilidadTotal: 30 },
{ cantidad: 7, utilidadTotal: 28 },
];
export function UtilidadTotalVsMarginal({ ejercicioId: _ejercicioId, onComplete }: UtilidadTotalVsMarginalProps) {
const [respuestas, setRespuestas] = useState<Record<number, string>>({});
const [verificadas, setVerificadas] = useState<Record<number, boolean>>({});
const [mostrarGrafico, setMostrarGrafico] = useState(false);
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const datosCompletos: FilaDatos[] = datosBase.map((fila, index) => ({
...fila,
utilidadMarginal: index === 0 ? null : fila.utilidadTotal - datosBase[index - 1].utilidadTotal
}));
const calcularUMg = useCallback((q: number) => {
const fila = datosCompletos.find(d => d.cantidad === q);
return fila?.utilidadMarginal ?? 0;
}, [datosCompletos]);
const handleRespuesta = (cantidad: number, valor: string) => {
setRespuestas(prev => ({ ...prev, [cantidad]: valor }));
setVerificadas(prev => ({ ...prev, [cantidad]: false }));
};
const verificarRespuesta = (cantidad: number) => {
const respuesta = parseFloat(respuestas[cantidad]);
const correcta = calcularUMg(cantidad);
const esCorrecta = Math.abs(respuesta - correcta) < 0.1;
setVerificadas(prev => ({ ...prev, [cantidad]: esCorrecta }));
const todasCorrectas = datosCompletos
.filter(d => d.cantidad > 0)
.every(d => {
const r = parseFloat(respuestas[d.cantidad]);
return Math.abs(r - calcularUMg(d.cantidad)) < 0.1;
});
if (todasCorrectas && onComplete) {
onComplete(100);
}
};
const puntaje = Object.values(verificadas).filter(Boolean).length;
const total = datosCompletos.length - 1;
const porcentaje = Math.round((puntaje / total) * 100);
const maxUT = Math.max(...datosCompletos.map(d => d.utilidadTotal));
const maxQ = Math.max(...datosCompletos.map(d => d.cantidad));
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Utilidad Total vs Utilidad Marginal"
subtitle="Comprende la relación entre la utilidad total acumulada y la utilidad adicional de cada unidad consumida"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">Conceptos Clave</h3>
<ul className="space-y-2 text-sm text-blue-800">
<li><strong>Utilidad Total (UT):</strong> Satisfacción total obtenida de consumir Q unidades de un bien.</li>
<li><strong>Utilidad Marginal (UMg):</strong> Utilidad adicional obtenida de consumir una unidad más.</li>
<li><strong>Fórmula:</strong> UMg = ΔUT / ΔQ = UT(Q) - UT(Q-1)</li>
</ul>
</div>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="border p-3 text-left">Cantidad (Q)</th>
<th className="border p-3 text-left">Utilidad Total (UT)</th>
<th className="border p-3 text-left">Calcular UMg</th>
<th className="border p-3 text-left">Estado</th>
</tr>
</thead>
<tbody>
{datosCompletos.map((fila) => (
<tr key={fila.cantidad} className={fila.cantidad === 0 ? 'bg-gray-50' : ''}>
<td className="border p-3 font-mono">{fila.cantidad}</td>
<td className="border p-3 font-mono">{fila.utilidadTotal}</td>
<td className="border p-3">
{fila.cantidad === 0 ? (
<span className="text-gray-500 text-sm">N/A (punto de partida)</span>
) : (
<div className="flex gap-2 items-center">
<Input
type="number"
step="0.1"
value={respuestas[fila.cantidad] || ''}
onChange={(e) => handleRespuesta(fila.cantidad, e.target.value)}
className="w-24"
placeholder="UMg"
/>
<Button
size="sm"
variant="outline"
onClick={() => verificarRespuesta(fila.cantidad)}
disabled={!respuestas[fila.cantidad]}
>
Verificar
</Button>
</div>
)}
</td>
<td className="border p-3">
{verificadas[fila.cantidad] === true && (
<span className="text-green-600 font-bold"> Correcto</span>
)}
{verificadas[fila.cantidad] === false && (
<span className="text-red-600 font-bold"> Incorrecto</span>
)}
{fila.cantidad > 0 && verificadas[fila.cantidad] === undefined && (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-sm text-gray-600 mb-2">
Progreso: <strong>{puntaje}/{total}</strong> correctas ({porcentaje}%)
</p>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${porcentaje}%` }}
/>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setMostrarGrafico(!mostrarGrafico)}
>
{mostrarGrafico ? 'Ocultar' : 'Ver'} Gráfico
</Button>
<Button
variant="outline"
onClick={() => setMostrarExplicacion(!mostrarExplicacion)}
>
{mostrarExplicacion ? 'Ocultar' : 'Ver'} Explicación
</Button>
</div>
{mostrarGrafico && (
<div className="bg-white border rounded-lg p-4">
<h4 className="font-bold mb-4">Gráfico de Utilidad Total</h4>
<div className="relative h-64 w-full">
<svg viewBox="0 0 400 250" className="w-full h-full">
<line x1="40" y1="220" x2="380" y2="220" stroke="#333" strokeWidth="2" />
<line x1="40" y1="220" x2="40" y2="20" stroke="#333" strokeWidth="2" />
<text x="200" y="245" textAnchor="middle" className="text-xs fill-gray-600">Cantidad (Q)</text>
<text x="15" y="125" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90, 15, 125)">Utilidad Total</text>
{datosCompletos.map((d, i) => {
const x = 60 + (d.cantidad / maxQ) * 300;
const y = 210 - (d.utilidadTotal / maxUT) * 180;
return (
<g key={d.cantidad}>
<circle cx={x} cy={y} r="4" fill="#3b82f6" />
<text x={x} y={y - 10} textAnchor="middle" className="text-xs fill-blue-600 font-mono">
{d.utilidadTotal}
</text>
<text x={x} y="235" textAnchor="middle" className="text-xs fill-gray-600">{d.cantidad}</text>
</g>
);
})}
<polyline
points={datosCompletos.map(d => {
const x = 60 + (d.cantidad / maxQ) * 300;
const y = 210 - (d.utilidadTotal / maxUT) * 180;
return `${x},${y}`;
}).join(' ')}
fill="none"
stroke="#3b82f6"
strokeWidth="2"
/>
</svg>
</div>
<p className="text-sm text-gray-600 mt-2">
Observa cómo la curva de utilidad total aumenta a tasas decrecientes hasta alcanzar su máximo en Q=5 y Q=6.
</p>
</div>
)}
{mostrarExplicacion && (
<div className="bg-yellow-50 border border-yellow-300 p-4 rounded-lg">
<h4 className="font-bold text-yellow-900 mb-2">Cálculo paso a paso:</h4>
<div className="space-y-2 text-sm text-yellow-800">
{datosCompletos.filter(d => d.cantidad > 0).map((fila) => (
<p key={fila.cantidad}>
<strong>Q={fila.cantidad}:</strong> UMg = UT({fila.cantidad}) - UT({fila.cantidad - 1}) = {fila.utilidadTotal} - {datosCompletos[fila.cantidad - 1].utilidadTotal} = <strong>{fila.utilidadMarginal}</strong>
</p>
))}
</div>
<div className="mt-4 p-3 bg-white rounded">
<p className="font-semibold">Puntos importantes:</p>
<ul className="list-disc ml-5 mt-1 space-y-1">
<li>La UMg es positiva mientras la UT esté aumentando (Q=1 a 5)</li>
<li>La UMg es cero cuando la UT es máxima (Q=6)</li>
<li>La UMg es negativa cuando la UT disminuye (Q=7)</li>
</ul>
</div>
</div>
)}
</div>
</Card>
);
}
export default UtilidadTotalVsMarginal;