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:
Renato
2026-02-12 06:58:29 +01:00
parent 0698eedcf4
commit aec6aef50f
104 changed files with 30129 additions and 50 deletions

View File

@@ -0,0 +1,265 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface Ejercicio {
id: number;
titulo: string;
descripcion: string;
i1: number;
i2: number;
q1: number;
q2: number;
unidadI: string;
unidadQ: string;
}
const ejercicios: Ejercicio[] = [
{
id: 1,
titulo: "Cálculo de Elasticidad Ingreso",
descripcion: "Cuando el ingreso mensual de una familia aumenta de $2,000 a $2,500, su consumo de carne aumenta de 8 kg a 12 kg mensuales.",
i1: 2000,
i2: 2500,
q1: 8,
q2: 12,
unidadI: "$/mes",
unidadQ: "kg"
},
{
id: 2,
titulo: "Elasticidad Ingreso - Producto Tecnológico",
descripcion: "El ingreso promedio de consumidores sube de $1,500 a $1,800 mensuales, y las ventas de smartphones premium aumentan de 50 a 80 unidades.",
i1: 1500,
i2: 1800,
q1: 50,
q2: 80,
unidadI: "$/mes",
unidadQ: "unidades"
},
{
id: 3,
titulo: "Elasticidad Ingreso - Transporte",
descripcion: "Cuando el ingreso familiar aumenta de $3,000 a $4,000 mensuales, el uso de transporte público disminuye de 40 a 25 viajes mensuales.",
i1: 3000,
i2: 4000,
q1: 40,
q2: 25,
unidadI: "$/mes",
unidadQ: "viajes"
}
];
interface Respuesta {
valor: string;
esCorrecta: boolean | null;
}
interface FormulaElasticidadIngresoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function FormulaElasticidadIngreso({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadIngresoProps) {
const [ejercicioActual, setEjercicioActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarSolucion, setMostrarSolucion] = useState<Record<number, boolean>>({});
const [mostrarFormula, setMostrarFormula] = useState(false);
const ejercicio = ejercicios[ejercicioActual];
const calcularElasticidad = (ej: Ejercicio) => {
const deltaQ = ej.q2 - ej.q1;
const deltaI = ej.i2 - ej.i1;
const qPromedio = (ej.q1 + ej.q2) / 2;
const iPromedio = (ej.i1 + ej.i2) / 2;
const porcentajeQ = (deltaQ / qPromedio) * 100;
const porcentajeI = (deltaI / iPromedio) * 100;
return porcentajeQ / porcentajeI;
};
const verificarRespuesta = () => {
const respuesta = respuestas[ejercicio.id];
if (!respuesta) return;
const valorCorrecto = calcularElasticidad(ejercicio);
const valorIngresado = parseFloat(respuesta.valor);
const esCorrecta = Math.abs(valorIngresado - valorCorrecto) <= 0.05;
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { ...respuesta, esCorrecta }
}));
if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) {
onComplete(100);
}
};
const handleRespuesta = (valor: string) => {
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { valor, esCorrecta: null }
}));
};
const toggleSolucion = () => {
setMostrarSolucion(prev => ({
...prev,
[ejercicio.id]: !prev[ejercicio.id]
}));
};
const siguienteEjercicio = () => {
if (ejercicioActual < ejercicios.length - 1) {
setEjercicioActual(prev => prev + 1);
}
};
const resultado = calcularElasticidad(ejercicio);
const respuestaActual = respuestas[ejercicio.id];
return (
<Card className="max-w-3xl mx-auto">
<CardHeader
title="Fórmula de Elasticidad Ingreso"
subtitle={`Ejercicio ${ejercicioActual + 1} de ${ejercicios.length}`}
/>
<div className="mb-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${((ejercicioActual + 1) / ejercicios.length) * 100}%` }}
/>
</div>
</div>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-lg text-blue-900">{ejercicio.titulo}</h3>
<p className="text-gray-700 mt-2">{ejercicio.descripcion}</p>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">I</span>
<p className="text-lg font-semibold">{ejercicio.i1.toLocaleString()} {ejercicio.unidadI}</p>
</div>
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">I</span>
<p className="text-lg font-semibold">{ejercicio.i2.toLocaleString()} {ejercicio.unidadI}</p>
</div>
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">Q</span>
<p className="text-lg font-semibold">{ejercicio.q1} {ejercicio.unidadQ}</p>
</div>
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">Q</span>
<p className="text-lg font-semibold">{ejercicio.q2} {ejercicio.unidadQ}</p>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-gray-700">Fórmula del método del punto medio:</h4>
<Button variant="ghost" size="sm" onClick={() => setMostrarFormula(!mostrarFormula)}>
{mostrarFormula ? 'Ocultar' : 'Mostrar'} fórmula
</Button>
</div>
{mostrarFormula && (
<div className="bg-white p-4 rounded border space-y-3">
<p className="font-mono text-center text-lg">
E<sub>i</sub> = (%ΔQ) / (%ΔI)
</p>
<div className="text-sm text-gray-600 space-y-1">
<p>Donde:</p>
<p> %ΔQ = [(Q - Q) / ((Q + Q) / 2)] × 100</p>
<p> %ΔI = [(I - I) / ((I + I) / 2)] × 100</p>
</div>
</div>
)}
</div>
<div className="border rounded-lg p-4">
<p className="text-gray-800 font-medium mb-3">
Calcule la elasticidad ingreso (E<sub>i</sub>):
</p>
<div className="flex gap-2">
<Input
type="number"
step="0.01"
value={respuestaActual?.valor || ''}
onChange={(e) => handleRespuesta(e.target.value)}
className="w-48"
placeholder="Respuesta"
/>
<Button
variant="outline"
onClick={verificarRespuesta}
disabled={!respuestaActual?.valor}
>
Verificar
</Button>
</div>
{respuestaActual?.esCorrecta !== null && (
<div className={`mt-3 p-3 rounded ${
respuestaActual.esCorrecta
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{respuestaActual.esCorrecta
? '¡Correcto!'
: 'Incorrecto. Revisa tus cálculos.'}
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={toggleSolucion}
className="mt-2"
>
{mostrarSolucion[ejercicio.id] ? 'Ocultar' : 'Ver'} solución paso a paso
</Button>
{mostrarSolucion[ejercicio.id] && (
<div className="mt-2 bg-gray-50 p-4 rounded text-sm space-y-2">
<p className="font-semibold">Desarrollo:</p>
<p className="font-mono">ΔQ = {ejercicio.q2} - {ejercicio.q1} = {ejercicio.q2 - ejercicio.q1}</p>
<p className="font-mono">ΔI = {ejercicio.i2} - {ejercicio.i1} = {ejercicio.i2 - ejercicio.i1}</p>
<p className="font-mono"> = ({ejercicio.q1} + {ejercicio.q2}) / 2 = {((ejercicio.q1 + ejercicio.q2) / 2).toFixed(1)}</p>
<p className="font-mono">Ī = ({ejercicio.i1} + {ejercicio.i2}) / 2 = {((ejercicio.i1 + ejercicio.i2) / 2).toFixed(1)}</p>
<p className="font-mono">%ΔQ = ({ejercicio.q2 - ejercicio.q1} / {((ejercicio.q1 + ejercicio.q2) / 2).toFixed(1)}) × 100 = {(((ejercicio.q2 - ejercicio.q1) / ((ejercicio.q1 + ejercicio.q2) / 2)) * 100).toFixed(2)}%</p>
<p className="font-mono">%ΔI = ({ejercicio.i2 - ejercicio.i1} / {((ejercicio.i1 + ejercicio.i2) / 2).toFixed(1)}) × 100 = {(((ejercicio.i2 - ejercicio.i1) / ((ejercicio.i1 + ejercicio.i2) / 2)) * 100).toFixed(2)}%</p>
<p className="font-mono font-bold text-blue-800">
E<sub>i</sub> = {(((ejercicio.q2 - ejercicio.q1) / ((ejercicio.q1 + ejercicio.q2) / 2)) * 100).toFixed(2)} / {(((ejercicio.i2 - ejercicio.i1) / ((ejercicio.i1 + ejercicio.i2) / 2)) * 100).toFixed(2)} = {resultado.toFixed(2)}
</p>
</div>
)}
</div>
<div className="flex justify-end">
{ejercicioActual < ejercicios.length - 1 ? (
<Button
onClick={siguienteEjercicio}
disabled={respuestaActual?.esCorrecta !== true}
>
Siguiente Ejercicio
</Button>
) : (
<div className="text-green-600 font-semibold">
{respuestaActual?.esCorrecta ? '¡Ejercicios completados!' : ''}
</div>
)}
</div>
</div>
</Card>
);
}
export default FormulaElasticidadIngreso;