244 lines
10 KiB
TypeScript
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;
|