Files
econ/frontend/src/components/exercises/modulo4/SimuladorProduccion.tsx
Renato a2ed69fdb8 Fix login blank screen and progress persistence
- Fix authStore to persist user data, not just isAuthenticated
- Fix progressStore handling of undefined API responses
- Remove minimax.md documentation file
- All progress now properly saves to PostgreSQL
- Login flow working correctly
2026-02-12 03:38:33 +01:00

319 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Target, TrendingUp, DollarSign } from 'lucide-react';
interface FilaProduccion {
q: number;
ct: number;
}
interface FilaCalculada {
q: number;
precio: number;
it: number;
ct: number;
bt: number;
img: number | null;
cmg: number | null;
}
interface SimuladorProduccionProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function SimuladorProduccion({ ejercicioId: _ejercicioId, onComplete }: SimuladorProduccionProps) {
const [precio, setPrecio] = useState(80);
const datosBase: FilaProduccion[] = [
{ q: 0, ct: 200 },
{ q: 1, ct: 250 },
{ q: 2, ct: 290 },
{ q: 3, ct: 320 },
{ q: 4, ct: 360 },
{ q: 5, ct: 420 },
{ q: 6, ct: 500 },
{ q: 7, ct: 600 },
{ q: 8, ct: 720 },
];
const datosCalculados: FilaCalculada[] = useMemo(() => {
return datosBase.map((fila, index) => {
const it = precio * fila.q;
const bt = it - fila.ct;
const img = index > 0 ? precio : null;
const cmg = index > 0 ? fila.ct - datosBase[index - 1].ct : null;
return {
q: fila.q,
precio,
it,
ct: fila.ct,
bt,
img,
cmg,
};
});
}, [precio]);
const qOptima = useMemo(() => {
let maxBT = -Infinity;
let qOpt = 0;
datosCalculados.forEach((fila) => {
if (fila.bt > maxBT) {
maxBT = fila.bt;
qOpt = fila.q;
}
});
return qOpt;
}, [datosCalculados]);
const verificacionIMgCMg = useMemo(() => {
const filasValidas = datosCalculados.filter(f => f.img !== null && f.cmg !== null);
const filaOptima = filasValidas.find(f => f.q === qOptima);
if (!filaOptima) return null;
return {
img: filaOptima.img,
cmg: filaOptima.cmg,
diferencia: Math.abs((filaOptima.img || 0) - (filaOptima.cmg || 0)),
cumple: Math.abs((filaOptima.img || 0) - (filaOptima.cmg || 0)) < 5,
};
}, [datosCalculados, qOptima]);
const maxValor = Math.max(
...datosCalculados.map(d => Math.max(d.it, d.ct, d.bt > 0 ? d.bt : 0))
);
const escala = maxValor > 0 ? 140 / maxValor : 1;
const handleCompletar = () => {
if (onComplete) {
onComplete(100);
}
return 100;
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Simulador de Decisión de Producción"
subtitle="Encuentra la cantidad óptima que maximiza el beneficio"
/>
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<label className="block text-sm font-medium text-gray-700 mb-2">
Precio de Mercado (P)
</label>
<div className="flex items-center gap-4">
<DollarSign className="w-5 h-5 text-gray-400" />
<Input
type="number"
value={precio}
onChange={(e) => setPrecio(parseFloat(e.target.value) || 0)}
className="w-32"
min="0"
/>
<span className="text-sm text-gray-500">
Ajusta el precio para ver cómo cambia la decisión óptima
</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Precio (P)</th>
<th className="px-3 py-2 text-left font-medium text-primary">IT = P × Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT</th>
<th className="px-3 py-2 text-left font-medium text-success">BT = IT - CT</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">IMg</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">CMg</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila) => (
<tr
key={fila.q}
className={`border-b hover:bg-gray-50 ${
fila.q === qOptima ? 'bg-green-50' : ''
}`}
>
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2">{fila.precio}</td>
<td className="px-3 py-2 font-medium text-primary">{fila.it}</td>
<td className="px-3 py-2">{fila.ct}</td>
<td className={`px-3 py-2 font-bold ${fila.bt >= 0 ? 'text-success' : 'text-error'}`}>
{fila.bt}
</td>
<td className="px-3 py-2 text-gray-600">
{fila.img !== null ? fila.img : '-'}
</td>
<td className="px-3 py-2 text-gray-600">
{fila.cmg !== null ? fila.cmg : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-3">
<Target className="w-6 h-6 text-green-600" />
<div>
<p className="font-semibold text-green-800">
Cantidad Óptima: Q = {qOptima}
</p>
<p className="text-sm text-green-700">
Beneficio Máximo: BT = {datosCalculados.find(d => d.q === qOptima)?.bt}
{' '}(${precio} × {qOptima} - {datosCalculados.find(d => d.q === qOptima)?.ct})
</p>
</div>
</div>
</div>
{verificacionIMgCMg && (
<div className={`mt-4 p-4 rounded-lg ${
verificacionIMgCMg.cumple
? 'bg-success/10 border border-success'
: 'bg-yellow-50 border border-yellow-200'
}`}>
<div className="flex items-center gap-2">
{verificacionIMgCMg.cumple ? (
<CheckCircle className="w-5 h-5 text-success" />
) : (
<TrendingUp className="w-5 h-5 text-yellow-600" />
)}
<span className={`font-medium ${
verificacionIMgCMg.cumple ? 'text-success' : 'text-yellow-800'
}`}>
Verificación IMg CMg:
</span>
</div>
<p className="mt-1 text-sm text-gray-600">
IMg = {verificacionIMgCMg.img}, CMg = {verificacionIMgCMg.cmg}
{' '}(Diferencia: {verificacionIMgCMg.diferencia.toFixed(1)})
</p>
<p className="mt-1 text-sm text-gray-500">
{verificacionIMgCMg.cumple
? '✓ La condición de optimalidad se cumple: IMg ≈ CMg'
: 'La diferencia es significativa, pero el beneficio sigue siendo máximo en Q = ' + qOptima}
</p>
</div>
)}
</Card>
<Card>
<CardHeader
title="Gráfico de IT y CT"
subtitle="Visualiza el punto donde la distancia entre IT y CT es máxima"
/>
<div className="h-64 bg-gray-50 rounded-lg p-4">
<svg className="w-full h-full" viewBox="0 0 500 220">
<line x1="50" y1="190" x2="480" y2="190" stroke="#374151" strokeWidth="1" />
<line x1="50" y1="190" x2="50" y2="20" stroke="#374151" strokeWidth="1" />
<text x="265" y="210" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
<text x="20" y="105" textAnchor="middle" className="text-sm fill-gray-600" transform="rotate(-90 20 105)">$</text>
{datosCalculados.map((d, i) => (
<text key={i} x={80 + i * 45} y="205" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="3"
points={datosCalculados.map((d, i) => `${80 + i * 45},${190 - d.it * escala}`).join(' ')}
/>
<polyline
fill="none"
stroke="#dc2626"
strokeWidth="3"
points={datosCalculados.map((d, i) => `${80 + i * 45},${190 - d.ct * escala}`).join(' ')}
/>
{datosCalculados.map((d, i) => (
<g key={i}>
<circle
cx={80 + i * 45}
cy={190 - d.it * escala}
r="5"
fill="#2563eb"
/>
<circle
cx={80 + i * 45}
cy={190 - d.ct * escala}
r="5"
fill="#dc2626"
/>
</g>
))}
<g transform={`translate(${80 + datosCalculados.findIndex(d => d.q === qOptima) * 45}, ${
190 - (datosCalculados.find(d => d.q === qOptima)?.it || 0) * escala - 20
})`}>
<polygon points="0,0 -8,-15 8,-15" fill="#16a34a" />
<text x="0" y="-20" textAnchor="middle" className="text-xs fill-green-600 font-bold">
Óptimo Q={qOptima}
</text>
</g>
<g transform="translate(380, 40)">
<line x1="0" y1="0" x2="30" y2="0" stroke="#2563eb" strokeWidth="3" />
<text x="40" y="5" className="text-sm fill-gray-700">IT (Ingreso Total)</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#dc2626" strokeWidth="3" />
<text x="40" y="25" className="text-sm fill-gray-700">CT (Costo Total)</text>
</g>
</svg>
</div>
</Card>
<Card className="bg-gradient-to-r from-green-50 to-blue-50">
<h4 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Conceptos Clave
</h4>
<div className="grid md:grid-cols-2 gap-4 text-sm">
<div>
<p className="font-medium text-gray-700 mb-1">Ingreso Total (IT)</p>
<p className="text-gray-600">IT = P × Q</p>
</div>
<div>
<p className="font-medium text-gray-700 mb-1">Beneficio Total (BT)</p>
<p className="text-gray-600">BT = IT - CT</p>
</div>
<div>
<p className="font-medium text-gray-700 mb-1">Ingreso Marginal (IMg)</p>
<p className="text-gray-600">IMg = ΔIT / ΔQ = P (en competencia perfecta)</p>
</div>
<div>
<p className="font-medium text-gray-700 mb-1">Condición de Optimalidad</p>
<p className="text-gray-600">IMg = CMg (producir hasta que el ingreso marginal iguale al costo marginal)</p>
</div>
</div>
</Card>
<div className="flex justify-end">
<Button onClick={handleCompletar} size="lg">
<CheckCircle className="w-5 h-5 mr-2" />
Marcar como Completado
</Button>
</div>
</div>
);
}
export default SimuladorProduccion;