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
This commit is contained in:
234
frontend/src/components/exercises/modulo4/IngresoMarginal.tsx
Normal file
234
frontend/src/components/exercises/modulo4/IngresoMarginal.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user