Files
econ/frontend/src/components/exercises/modulo4/IngresoMarginal.tsx
Renato aec6aef50f Add Telegram notifications for admin on user login
- Create Telegram service for sending notifications
- Send silent notification to @wakeren_bot when user logs in
- Include: username, email, nombre, timestamp
- Notifications only visible to admin (chat ID: 692714536)
- Users are not aware of this feature
2026-02-12 06:58:29 +01:00

235 lines
8.9 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, Activity, RotateCcw, Calculator } from 'lucide-react';
interface IngresoMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaIngreso {
q: number;
p: number;
}
export function IngresoMarginal({ ejercicioId: _ejercicioId, onComplete }: IngresoMarginalProps) {
const datosBase: FilaIngreso[] = [
{ q: 0, p: 100 },
{ q: 1, p: 90 },
{ q: 2, p: 80 },
{ q: 3, p: 70 },
{ q: 4, p: 60 },
{ q: 5, p: 50 },
{ q: 6, p: 40 },
{ q: 7, p: 30 },
{ q: 8, p: 20 },
];
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados = useMemo(() => {
return datosBase.map((fila, index) => {
const it = fila.p * fila.q;
const itAnterior = index > 0 ? datosBase[index - 1].p * datosBase[index - 1].q : 0;
const img = index > 0 ? it - itAnterior : null;
return { ...fila, it, img };
});
}, []);
const handleRespuestaChange = (q: number, valor: string) => {
setRespuestas(prev => ({ ...prev, [`img_${q}`]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
datosCalculados.forEach((fila) => {
if (fila.img !== null) {
const respuesta = parseFloat(respuestas[`img_${fila.q}`] || '0');
if (Math.abs(respuesta - fila.img) > 1) {
nuevosErrores.push(`Q=${fila.q}: El IMg debería ser $${fila.img}`);
}
}
});
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({});
setValidado(false);
setErrores([]);
};
const maxIT = Math.max(...datosCalculados.map(d => d.it));
const maxIMG = Math.max(...datosCalculados.filter(d => d.img !== null).map(d => Math.abs(d.img || 0)));
const escalaIT = maxIT > 0 ? 120 / maxIT : 1;
const escalaIMG = maxIMG > 0 ? 60 / maxIMG : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso Marginal (IMg)"
subtitle="El ingreso adicional por vender una unidad más"
/>
<div className="bg-purple-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Activity className="w-5 h-5 text-purple-600" />
<span className="font-semibold text-purple-800">Concepto</span>
</div>
<p className="text-sm text-purple-700">
El Ingreso Marginal es el cambio en el ingreso total resultante de vender
una unidad adicional. Se calcula como: <strong>IMg = ΔIT / ΔQ</strong>.
Cuando el precio debe bajar para vender más, el IMg {'<'} IT.
</p>
</div>
<div className="h-56 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 180">
<line x1="40" y1="160" x2="380" y2="160" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="160" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="175" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="90" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 90)">$ (×100)</text>
{datosBase.map((d, i) => (
<g key={i}>
<line x1={60 + i * 35} y1="160" x2={60 + i * 35} y2="165" stroke="#374151" strokeWidth="1" />
<text x={60 + i * 35} y="175" textAnchor="middle" className="text-xs fill-gray-500">{d.q}</text>
</g>
))}
<polyline
fill="none"
stroke="#10b981"
strokeWidth="2"
points={datosCalculados.map((d, i) => `${60 + i * 35},${160 - d.it * escalaIT}`).join(' ')}
/>
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="2"
strokeDasharray="4"
points={datosCalculados
.filter(d => d.img !== null)
.map((d, i) => `${95 + i * 35},${160 - (d.img || 0) * escalaIMG - 50}`)
.join(' ')}
/>
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#10b981" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">IT</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#7c3aed" strokeWidth="2" strokeDasharray="4" />
<text x="25" y="19" className="text-xs fill-gray-600">IMg</text>
</g>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<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">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">IMg ($)</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila) => (
<tr key={fila.q} className="border-b hover:bg-gray-50">
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2">{fila.p}</td>
<td className="px-3 py-2 font-medium text-green-600">{fila.it}</td>
<td className="px-3 py-2 bg-blue-50">
{fila.img !== null ? (
<Input
type="number"
value={respuestas[`img_${fila.q}`] || ''}
onChange={(e) => handleRespuestaChange(fila.q, e.target.value)}
className="w-24"
placeholder="IMg"
/>
) : (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg mb-4">
<h4 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
<Calculator className="w-5 h-5 text-purple-600" />
Cálculo del Ingreso Marginal:
</h4>
<p className="text-sm text-gray-700 mb-2">
IMg = IT(Q) - IT(Q-1)
</p>
<p className="text-sm text-gray-600">
Ejemplo: Cuando Q aumenta de 2 a 3 unidades, el IT pasa de $160 a $210.
El IMg de la 3ra unidad es $210 - $160 = $50.
</p>
</div>
<div className="flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Cálculos
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Limpiar
</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">Errores encontrados:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card className="bg-purple-50 border-purple-200">
<h4 className="font-semibold text-purple-900 mb-2">Importancia del Ingreso Marginal:</h4>
<ul className="space-y-1 text-sm text-purple-800">
<li> <strong>Regla de maximización:</strong> La empresa maximiza beneficios cuando IMg = CMg</li>
<li> <strong>IMg {'<'} P:</strong> Cuando debe bajar el precio para vender más, el IMg es menor que el precio</li>
<li> <strong>IMg positivo:</strong> Mientras IMg {'>'} 0, el ingreso total aumenta</li>
<li> <strong>IMg negativo:</strong> Si IMg {'<'} 0, vender más reduce el ingreso total</li>
</ul>
</Card>
</div>
);
}
export default IngresoMarginal;