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
This commit is contained in:
328
frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Normal file
328
frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { CheckCircle, RotateCcw, Calculator } from 'lucide-react';
|
||||
|
||||
interface FilaCostos {
|
||||
q: number;
|
||||
cv: number;
|
||||
}
|
||||
|
||||
interface FilaCalculada extends FilaCostos {
|
||||
cf: number;
|
||||
ct: number;
|
||||
cfme: number;
|
||||
cvme: number;
|
||||
cme: number;
|
||||
cmg: number | null;
|
||||
}
|
||||
|
||||
interface CalculadoraCostosProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function CalculadoraCostos({ ejercicioId: _ejercicioId, onComplete }: CalculadoraCostosProps) {
|
||||
const CF_BASE = 200;
|
||||
|
||||
const [filas, setFilas] = useState<FilaCostos[]>([
|
||||
{ q: 0, cv: 0 },
|
||||
{ q: 1, cv: 50 },
|
||||
{ q: 2, cv: 90 },
|
||||
{ q: 3, cv: 120 },
|
||||
{ q: 4, cv: 160 },
|
||||
{ q: 5, cv: 220 },
|
||||
{ q: 6, cv: 300 },
|
||||
{ q: 7, cv: 400 },
|
||||
{ q: 8, cv: 520 },
|
||||
]);
|
||||
|
||||
const [validado, setValidado] = useState(false);
|
||||
const [errores, setErrores] = useState<string[]>([]);
|
||||
|
||||
const datosCalculados: FilaCalculada[] = useMemo(() => {
|
||||
return filas.map((fila, index) => {
|
||||
const ct = CF_BASE + fila.cv;
|
||||
const cfme = fila.q > 0 ? CF_BASE / fila.q : 0;
|
||||
const cvme = fila.q > 0 ? fila.cv / fila.q : 0;
|
||||
const cme = fila.q > 0 ? ct / fila.q : 0;
|
||||
const cmg = index > 0 ? ct - (CF_BASE + filas[index - 1].cv) : null;
|
||||
|
||||
return {
|
||||
...fila,
|
||||
cf: CF_BASE,
|
||||
ct,
|
||||
cfme,
|
||||
cvme,
|
||||
cme,
|
||||
cmg,
|
||||
};
|
||||
});
|
||||
}, [filas]);
|
||||
|
||||
const handleCvChange = (index: number, valor: string) => {
|
||||
const numValor = parseFloat(valor) || 0;
|
||||
const nuevasFilas = [...filas];
|
||||
nuevasFilas[index] = { ...nuevasFilas[index], cv: numValor };
|
||||
setFilas(nuevasFilas);
|
||||
setValidado(false);
|
||||
};
|
||||
|
||||
const validarCalculos = () => {
|
||||
const nuevosErrores: string[] = [];
|
||||
|
||||
datosCalculados.forEach((fila, index) => {
|
||||
if (fila.ct !== fila.cf + fila.cv) {
|
||||
nuevosErrores.push(`Fila ${index + 1}: CT no coincide con CF + CV`);
|
||||
}
|
||||
if (fila.q > 0 && Math.abs(fila.cme - fila.ct / fila.q) > 0.01) {
|
||||
nuevosErrores.push(`Fila ${index + 1}: CMe calculado incorrectamente`);
|
||||
}
|
||||
});
|
||||
|
||||
setErrores(nuevosErrores);
|
||||
setValidado(true);
|
||||
|
||||
if (nuevosErrores.length === 0) {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reiniciar = () => {
|
||||
setFilas([
|
||||
{ q: 0, cv: 0 },
|
||||
{ q: 1, cv: 50 },
|
||||
{ q: 2, cv: 90 },
|
||||
{ q: 3, cv: 120 },
|
||||
{ q: 4, cv: 160 },
|
||||
{ q: 5, cv: 220 },
|
||||
{ q: 6, cv: 300 },
|
||||
{ q: 7, cv: 400 },
|
||||
{ q: 8, cv: 520 },
|
||||
]);
|
||||
setValidado(false);
|
||||
setErrores([]);
|
||||
};
|
||||
|
||||
const maxCT = Math.max(...datosCalculados.map(d => d.ct));
|
||||
const maxCMe = Math.max(...datosCalculados.filter(d => d.q > 0).map(d => d.cme));
|
||||
const maxCMg = Math.max(...datosCalculados.filter(d => d.cmg !== null).map(d => d.cmg || 0));
|
||||
const escalaCT = maxCT > 0 ? 150 / maxCT : 1;
|
||||
const escalaCMe = maxCMe > 0 ? 150 / maxCMe : 1;
|
||||
const escalaCMg = maxCMg > 0 ? 150 / maxCMg : 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Calculadora de Costos"
|
||||
subtitle="Ingresa los Costos Variables (CV) y observa los cálculos automáticos"
|
||||
/>
|
||||
|
||||
<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">CF</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">CV</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-gray-700">CFMe</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CVMe</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CMe</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CMg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{datosCalculados.map((fila, index) => (
|
||||
<tr key={index} className="border-b hover:bg-gray-50">
|
||||
<td className="px-3 py-2 font-medium">{fila.q}</td>
|
||||
<td className="px-3 py-2 text-gray-600">{fila.cf}</td>
|
||||
<td className="px-3 py-2 bg-blue-50">
|
||||
<input
|
||||
type="number"
|
||||
value={fila.cv}
|
||||
onChange={(e) => handleCvChange(index, e.target.value)}
|
||||
className="w-20 px-2 py-1 border rounded text-sm focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
min="0"
|
||||
disabled={fila.q === 0}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-primary">{fila.ct}</td>
|
||||
<td className="px-3 py-2 text-gray-600">
|
||||
{fila.q > 0 ? fila.cfme.toFixed(2) : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-600">
|
||||
{fila.q > 0 ? fila.cvme.toFixed(2) : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-secondary">
|
||||
{fila.q > 0 ? fila.cme.toFixed(2) : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-success">
|
||||
{fila.cmg !== null ? fila.cmg : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-3">
|
||||
<Button onClick={validarCalculos} variant="primary">
|
||||
<Calculator className="w-4 h-4 mr-2" />
|
||||
Validar Cálculos
|
||||
</Button>
|
||||
<Button onClick={reiniciar} variant="outline">
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Reiniciar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{validado && errores.length === 0 && (
|
||||
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
|
||||
<div className="flex items-center gap-2 text-success">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
<span className="font-medium">¡Todos los cálculos son correctos!</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validado && errores.length > 0 && (
|
||||
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
|
||||
<p className="font-medium text-error mb-2">Se encontraron errores:</p>
|
||||
<ul className="list-disc list-inside text-sm text-error">
|
||||
{errores.map((error, i) => (
|
||||
<li key={i}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Visualización de Curvas de Costos"
|
||||
subtitle="Gráfico de CT, CMe y CMg"
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Total (CT)</h4>
|
||||
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
|
||||
<svg className="w-full h-full" viewBox="0 0 400 160">
|
||||
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
|
||||
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
|
||||
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
|
||||
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">CT</text>
|
||||
|
||||
{datosCalculados.map((d, i) => (
|
||||
<text key={i} x={30 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
|
||||
{d.q}
|
||||
</text>
|
||||
))}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="2"
|
||||
points={datosCalculados.map((d, i) => `${30 + i * 40},${140 - d.ct * escalaCT}`).join(' ')}
|
||||
/>
|
||||
|
||||
{datosCalculados.map((d, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={30 + i * 40}
|
||||
cy={140 - d.ct * escalaCT}
|
||||
r="4"
|
||||
fill="#2563eb"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Medio (CMe) vs Costo Marginal (CMg)</h4>
|
||||
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
|
||||
<svg className="w-full h-full" viewBox="0 0 400 160">
|
||||
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
|
||||
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
|
||||
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
|
||||
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">Costo</text>
|
||||
|
||||
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
|
||||
<text key={i} x={70 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
|
||||
{d.q}
|
||||
</text>
|
||||
))}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#7c3aed"
|
||||
strokeWidth="2"
|
||||
points={datosCalculados
|
||||
.filter(d => d.q > 0)
|
||||
.map((d, i) => `${70 + i * 40},${140 - d.cme * escalaCMe}`)
|
||||
.join(' ')}
|
||||
/>
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="4"
|
||||
points={datosCalculados
|
||||
.filter(d => d.cmg !== null)
|
||||
.map((d, i) => `${70 + i * 40},${140 - (d.cmg || 0) * escalaCMg}`)
|
||||
.join(' ')}
|
||||
/>
|
||||
|
||||
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
|
||||
<circle
|
||||
key={`cme-${i}`}
|
||||
cx={70 + i * 40}
|
||||
cy={140 - d.cme * escalaCMe}
|
||||
r="4"
|
||||
fill="#7c3aed"
|
||||
/>
|
||||
))}
|
||||
|
||||
{datosCalculados.filter(d => d.cmg !== null).map((d, i) => (
|
||||
<circle
|
||||
key={`cmg-${i}`}
|
||||
cx={70 + i * 40}
|
||||
cy={140 - (d.cmg || 0) * escalaCMg}
|
||||
r="4"
|
||||
fill="#16a34a"
|
||||
/>
|
||||
))}
|
||||
|
||||
<g transform="translate(280, 30)">
|
||||
<line x1="0" y1="0" x2="20" y2="0" stroke="#7c3aed" strokeWidth="2" />
|
||||
<text x="25" y="4" className="text-xs fill-gray-600">CMe</text>
|
||||
<line x1="0" y1="15" x2="20" y2="15" stroke="#16a34a" strokeWidth="2" strokeDasharray="4" />
|
||||
<text x="25" y="19" className="text-xs fill-gray-600">CMg</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas utilizadas:</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li><strong>CT</strong> = CF + CV (Costo Total)</li>
|
||||
<li><strong>CFMe</strong> = CF / Q (Costo Fijo Medio)</li>
|
||||
<li><strong>CVMe</strong> = CV / Q (Costo Variable Medio)</li>
|
||||
<li><strong>CMe</strong> = CT / Q (Costo Medio)</li>
|
||||
<li><strong>CMg</strong> = ΔCT / ΔQ (Costo Marginal)</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalculadoraCostos;
|
||||
@@ -0,0 +1,318 @@
|
||||
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;
|
||||
@@ -0,0 +1,344 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Input } from '../../ui/Input';
|
||||
import { CheckCircle, Info, TrendingUp } from 'lucide-react';
|
||||
|
||||
interface VisualizadorExcedentesProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function VisualizadorExcedentes({ ejercicioId: _ejercicioId, onComplete }: VisualizadorExcedentesProps) {
|
||||
const [precio, setPrecio] = useState(50);
|
||||
|
||||
const demandaParams = { a: 100, b: 1 };
|
||||
const ofertaParams = { c: 10, d: 0.8 };
|
||||
|
||||
const puntoEquilibrio = useMemo(() => {
|
||||
const { a, b } = demandaParams;
|
||||
const { c, d } = ofertaParams;
|
||||
const pEq = (a - c) / (b + d);
|
||||
const qEq = a - b * pEq;
|
||||
return { pEq, qEq };
|
||||
}, []);
|
||||
|
||||
const datosCurvas = useMemo(() => {
|
||||
const puntos = [];
|
||||
const { a, b } = demandaParams;
|
||||
const { c, d } = ofertaParams;
|
||||
|
||||
for (let q = 0; q <= 100; q += 5) {
|
||||
const pDemanda = (a - q) / b;
|
||||
const pOferta = q > 0 ? (q - c) / d : 0;
|
||||
puntos.push({ q, pDemanda: Math.max(0, pDemanda), pOferta: Math.max(0, pOferta) });
|
||||
}
|
||||
|
||||
return puntos;
|
||||
}, []);
|
||||
|
||||
const excedentes = useMemo(() => {
|
||||
const { a } = demandaParams;
|
||||
const { c } = ofertaParams;
|
||||
|
||||
const qAlPrecio = Math.max(0, a - demandaParams.b * precio);
|
||||
const qOfrecida = Math.max(0, c + ofertaParams.d * precio);
|
||||
|
||||
const excedenteConsumidor = 0.5 * qAlPrecio * (a - precio);
|
||||
const excedenteProductor = 0.5 * qOfrecida * (precio - c);
|
||||
|
||||
return {
|
||||
ec: excedenteConsumidor,
|
||||
ep: excedenteProductor,
|
||||
total: excedenteConsumidor + excedenteProductor,
|
||||
qAlPrecio,
|
||||
qOfrecida,
|
||||
};
|
||||
}, [precio]);
|
||||
|
||||
const excedentesEquilibrio = useMemo(() => {
|
||||
const { pEq, qEq } = puntoEquilibrio;
|
||||
const { a } = demandaParams;
|
||||
const { c } = ofertaParams;
|
||||
|
||||
const ec = 0.5 * qEq * (a - pEq);
|
||||
const ep = 0.5 * qEq * (pEq - c);
|
||||
|
||||
return { ec, ep, total: ec + ep };
|
||||
}, [puntoEquilibrio]);
|
||||
|
||||
const maxP = 100;
|
||||
const maxQ = 100;
|
||||
const escalaX = 350 / maxQ;
|
||||
const escalaY = 180 / maxP;
|
||||
|
||||
const handleCompletar = () => {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
return 100;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Visualizador de Excedentes"
|
||||
subtitle="Ajusta el precio para ver cómo cambian los excedentes del consumidor y productor"
|
||||
/>
|
||||
|
||||
<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">
|
||||
<Input
|
||||
type="range"
|
||||
min="20"
|
||||
max="90"
|
||||
value={precio}
|
||||
onChange={(e) => setPrecio(parseFloat(e.target.value))}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-2xl font-bold text-primary min-w-[80px]">${precio}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>$20</span>
|
||||
<span>Precio de equilibrio: ${puntoEquilibrio.pEq.toFixed(1)}</span>
|
||||
<span>$90</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-80 bg-gray-50 rounded-lg p-4 overflow-hidden">
|
||||
<svg className="w-full h-full" viewBox="0 0 400 220" preserveAspectRatio="xMidYMid meet">
|
||||
<line x1="50" y1="200" x2="380" y2="200" stroke="#374151" strokeWidth="2" />
|
||||
<line x1="50" y1="200" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
|
||||
|
||||
<text x="215" y="215" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
|
||||
<text x="15" y="110" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 15 110)">Precio (P)</text>
|
||||
|
||||
{[0, 25, 50, 75, 100].map((q) => (
|
||||
<g key={q}>
|
||||
<line
|
||||
x1={50 + q * escalaX}
|
||||
y1="200"
|
||||
x2={50 + q * escalaX}
|
||||
y2="205"
|
||||
stroke="#374151"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
x={50 + q * escalaX}
|
||||
y="215"
|
||||
textAnchor="middle"
|
||||
className="text-xs fill-gray-500"
|
||||
>
|
||||
{q}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{[0, 25, 50, 75, 100].map((p) => (
|
||||
<g key={p}>
|
||||
<line
|
||||
x1="45"
|
||||
y1={200 - p * escalaY}
|
||||
x2="50"
|
||||
y2={200 - p * escalaY}
|
||||
stroke="#374151"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
x="40"
|
||||
y={200 - p * escalaY + 4}
|
||||
textAnchor="end"
|
||||
className="text-xs fill-gray-500"
|
||||
>
|
||||
{p}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
<line
|
||||
x1="50"
|
||||
y1={200 - precio * escalaY}
|
||||
x2="380"
|
||||
y2={200 - precio * escalaY}
|
||||
stroke="#7c3aed"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
opacity="0.7"
|
||||
/>
|
||||
<text x="385" y={200 - precio * escalaY + 4} className="text-xs fill-purple-600 font-medium">
|
||||
P = {precio}
|
||||
</text>
|
||||
|
||||
{precio > puntoEquilibrio.pEq && (
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - precio * escalaY}
|
||||
${50 + excedentes.qAlPrecio * escalaX},${200 - precio * escalaY}
|
||||
${50 + excedentes.qAlPrecio * escalaX},${200 - ((100 - excedentes.qAlPrecio)) * escalaY}
|
||||
`}
|
||||
fill="rgba(37, 99, 235, 0.3)"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{precio < puntoEquilibrio.pEq && (
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - 12.5 * escalaY}
|
||||
${50 + excedentes.qOfrecida * escalaX},${200 - precio * escalaY}
|
||||
50,${200 - precio * escalaY}
|
||||
`}
|
||||
fill="rgba(22, 163, 74, 0.3)"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{Math.abs(precio - puntoEquilibrio.pEq) < 2 && (
|
||||
<>
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - puntoEquilibrio.pEq * escalaY}
|
||||
${50 + puntoEquilibrio.qEq * escalaX},${200 - puntoEquilibrio.pEq * escalaY}
|
||||
${50 + puntoEquilibrio.qEq * escalaX},${200 - 100 * escalaY}
|
||||
`}
|
||||
fill="rgba(37, 99, 235, 0.3)"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - 12.5 * escalaY}
|
||||
${50 + puntoEquilibrio.qEq * escalaX},${200 - puntoEquilibrio.pEq * escalaY}
|
||||
50,${200 - puntoEquilibrio.pEq * escalaY}
|
||||
`}
|
||||
fill="rgba(22, 163, 74, 0.3)"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="3"
|
||||
points={datosCurvas.map(d => `${50 + d.q * escalaX},${200 - d.pDemanda * escalaY}`).join(' ')}
|
||||
/>
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="3"
|
||||
points={datosCurvas.filter(d => d.pOferta >= 0).map(d => `${50 + d.q * escalaX},${200 - d.pOferta * escalaY}`).join(' ')}
|
||||
/>
|
||||
|
||||
<circle
|
||||
cx={50 + puntoEquilibrio.qEq * escalaX}
|
||||
cy={200 - puntoEquilibrio.pEq * escalaY}
|
||||
r="6"
|
||||
fill="#dc2626"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
x={50 + puntoEquilibrio.qEq * escalaX}
|
||||
y={200 - puntoEquilibrio.pEq * escalaY - 12}
|
||||
textAnchor="middle"
|
||||
className="text-xs fill-red-600 font-bold"
|
||||
>
|
||||
E
|
||||
</text>
|
||||
|
||||
<g transform="translate(320, 40)">
|
||||
<line x1="0" y1="0" x2="25" y2="0" stroke="#2563eb" strokeWidth="3" />
|
||||
<text x="30" y="5" className="text-xs fill-gray-700">Demanda</text>
|
||||
<line x1="0" y1="15" x2="25" y2="15" stroke="#16a34a" strokeWidth="3" />
|
||||
<text x="30" y="20" className="text-xs fill-gray-700">Oferta</text>
|
||||
<rect x="0" y="30" width="15" height="15" fill="rgba(37, 99, 235, 0.3)" stroke="#2563eb" />
|
||||
<text x="20" y="42" className="text-xs fill-gray-700">EC</text>
|
||||
<rect x="0" y="50" width="15" height="15" fill="rgba(22, 163, 74, 0.3)" stroke="#16a34a" />
|
||||
<text x="20" y="62" className="text-xs fill-gray-700">EP</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-4 h-4 bg-blue-500/30 border border-blue-500 rounded" />
|
||||
<h4 className="font-semibold text-blue-900">Excedente del Consumidor</h4>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-blue-700">${excedentes.ec.toFixed(0)}</p>
|
||||
<p className="text-sm text-blue-600 mt-1">
|
||||
Área bajo la curva de demanda y sobre el precio
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-green-50 border-green-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-4 h-4 bg-green-500/30 border border-green-500 rounded" />
|
||||
<h4 className="font-semibold text-green-900">Excedente del Productor</h4>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-green-700">${excedentes.ep.toFixed(0)}</p>
|
||||
<p className="text-sm text-green-600 mt-1">
|
||||
Área sobre la curva de oferta y bajo el precio
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-purple-50 border-purple-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||
<h4 className="font-semibold text-purple-900">Excedente Total</h4>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-purple-700">${excedentes.total.toFixed(0)}</p>
|
||||
<p className="text-sm text-purple-600 mt-1">
|
||||
EC + EP = Bienestar social total
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-yellow-50 border-yellow-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="w-5 h-5 text-yellow-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-yellow-900 mb-2">En el Equilibrio de Mercado:</h4>
|
||||
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-yellow-800">Precio: </span>
|
||||
<span className="font-bold">${puntoEquilibrio.pEq.toFixed(1)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-yellow-800">Cantidad: </span>
|
||||
<span className="font-bold">{puntoEquilibrio.qEq.toFixed(1)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-yellow-800">Excedente Total: </span>
|
||||
<span className="font-bold">${excedentesEquilibrio.total.toFixed(0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-yellow-800 text-sm">
|
||||
El equilibrio de mercado maximiza el bienestar social (excedente total).
|
||||
Cualquier desviación del precio de equilibrio genera pérdida de eficiencia.
|
||||
</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 VisualizadorExcedentes;
|
||||
3
frontend/src/components/exercises/modulo4/index.ts
Normal file
3
frontend/src/components/exercises/modulo4/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CalculadoraCostos } from './CalculadoraCostos';
|
||||
export { SimuladorProduccion } from './SimuladorProduccion';
|
||||
export { VisualizadorExcedentes } from './VisualizadorExcedentes';
|
||||
Reference in New Issue
Block a user