Initial commit - cleaned for CV
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user