- 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
319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
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;
|