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,309 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, ArrowUp, RotateCcw, AlertTriangle } from 'lucide-react';
interface DiseconomiasEscalaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface RangoEscala {
min: number;
max: number;
tipo: 'economias' | 'constante' | 'diseconomias';
descripcion: string;
}
export function DiseconomiasEscala({ ejercicioId: _ejercicioId, onComplete }: DiseconomiasEscalaProps) {
const rangos: RangoEscala[] = [
{ min: 0, max: 500, tipo: 'economias', descripcion: 'Economías de escala' },
{ min: 500, max: 1000, tipo: 'constante', descripcion: 'Rendimientos constantes a escala' },
{ min: 1000, max: 2000, tipo: 'diseconomias', descripcion: 'Diseconomías de escala' },
];
const calcularCMe = (q: number): number => {
if (q <= 500) {
return 50 - (q / 500) * 20;
} else if (q <= 1000) {
return 30;
} else {
return 30 + ((q - 1000) / 1000) * 25;
}
};
const [cantidad, setCantidad] = useState(600);
const [respuestas, setRespuestas] = useState({
cme: '',
ct: '',
rango: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const cmeActual = useMemo(() => calcularCMe(cantidad), [cantidad]);
const ctActual = useMemo(() => cmeActual * cantidad, [cmeActual, cantidad]);
const rangoActual = useMemo(() => {
return rangos.find(r => cantidad >= r.min && cantidad < r.max) || rangos[2];
}, [cantidad]);
const datosGrafico = useMemo(() => {
const puntos = [];
for (let q = 100; q <= 2000; q += 100) {
puntos.push({ q, cme: calcularCMe(q) });
}
return puntos;
}, []);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (Math.abs(parseFloat(respuestas.cme) - cmeActual) > 0.5) {
nuevosErrores.push(`El CMe no es correcto. Debería ser aproximadamente $${cmeActual.toFixed(2)}`);
}
if (Math.abs(parseFloat(respuestas.ct) - ctActual) > 50) {
nuevosErrores.push(`El CT no es correcto. Recuerda: CT = CMe × Q`);
}
if (respuestas.rango.toLowerCase() !== rangoActual.tipo.toLowerCase()) {
nuevosErrores.push(`El rango no es correcto. Estás en la zona de ${rangoActual.descripcion}`);
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setCantidad(600);
setRespuestas({ cme: '', ct: '', rango: '' });
setValidado(false);
setErrores([]);
};
const maxCMe = Math.max(...datosGrafico.map(d => d.cme));
const escalaY = 100 / maxCMe;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Diseconomías de Escala"
subtitle="Aumento del costo medio cuando la empresa crece demasiado"
/>
<div className="bg-red-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<ArrowUp className="w-5 h-5 text-red-600" />
<span className="font-semibold text-red-800">Concepto</span>
</div>
<p className="text-sm text-red-700">
Las deseconomías de escala ocurren cuando la empresa crece tanto que los costos de
coordinación, supervisión y comunición aumentan. El CMe comienza a subir después
de alcanzar el punto óptimo de escala.
</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Cantidad producida (Q)
</label>
<div className="flex items-center gap-4">
<input
type="range"
min="100"
max="2000"
step="100"
value={cantidad}
onChange={(e) => {
setCantidad(parseInt(e.target.value));
setValidado(false);
}}
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<span className="font-mono text-lg font-bold text-primary w-20 text-center">
{cantidad}
</span>
</div>
</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)">CMe ($)</text>
{[500, 1000, 1500, 2000].map((q) => (
<g key={q}>
<line x1={40 + (q / 2000) * 300} y1="160" x2={40 + (q / 2000) * 300} y2="165" stroke="#374151" strokeWidth="1" />
<text x={40 + (q / 2000) * 300} y="175" textAnchor="middle" className="text-xs fill-gray-500">{q}</text>
</g>
))}
{[10, 20, 30, 40, 50].map((val) => (
<g key={val}>
<line x1="35" y1={160 - val * escalaY * 2} x2="40" y2={160 - val * escalaY * 2} stroke="#374151" strokeWidth="1" />
<text x="30" y={165 - val * escalaY * 2} textAnchor="end" className="text-xs fill-gray-500">{val}</text>
</g>
))}
<rect x="40" y="20" width="75" height="140" fill="green" fillOpacity="0.1" />
<rect x="115" y="20" width="75" height="140" fill="blue" fillOpacity="0.1" />
<rect x="190" y="20" width="190" height="140" fill="red" fillOpacity="0.1" />
<text x="77" y="35" textAnchor="middle" className="text-xs fill-green-700 font-medium">Economías</text>
<text x="152" y="35" textAnchor="middle" className="text-xs fill-blue-700 font-medium">Constantes</text>
<text x="285" y="35" textAnchor="middle" className="text-xs fill-red-700 font-medium">Diseconomías</text>
<path
d={`M ${datosGrafico.map((d, i) => `${40 + (d.q / 2000) * 300},${160 - d.cme * escalaY * 2}`).join(' L ')}`}
fill="none"
stroke="#7c3aed"
strokeWidth="3"
/>
<circle
cx={40 + (cantidad / 2000) * 300}
cy={160 - cmeActual * escalaY * 2}
r="6"
fill="#10b981"
stroke="white"
strokeWidth="3"
/>
<line
x1={40 + (cantidad / 2000) * 300}
y1={160 - cmeActual * escalaY * 2}
x2={40 + (cantidad / 2000) * 300}
y2="160"
stroke="#10b981"
strokeWidth="2"
strokeDasharray="4"
/>
</svg>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 p-4 rounded-lg text-center">
<p className="text-sm text-blue-600 mb-1">Costo Medio</p>
<p className="text-3xl font-bold text-blue-800">${cmeActual.toFixed(2)}</p>
</div>
<div className="bg-purple-50 p-4 rounded-lg text-center">
<p className="text-sm text-purple-600 mb-1">Costo Total</p>
<p className="text-3xl font-bold text-purple-800">${ctActual.toLocaleString()}</p>
</div>
<div className={`p-4 rounded-lg text-center ${
rangoActual.tipo === 'economias' ? 'bg-green-50' :
rangoActual.tipo === 'diseconomias' ? 'bg-red-50' : 'bg-blue-50'
}`}>
<p className="text-sm text-gray-600 mb-1">Zona</p>
<p className={`text-lg font-bold ${
rangoActual.tipo === 'economias' ? 'text-green-800' :
rangoActual.tipo === 'diseconomias' ? 'text-red-800' : 'text-blue-800'
}`}>
{rangoActual.descripcion}
</p>
</div>
</div>
<div className="bg-gradient-to-r from-orange-50 to-red-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-orange-600" />
Responde para Q = {cantidad}:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Costo Medio ($)
</label>
<Input
type="number"
step="0.01"
value={respuestas.cme}
onChange={(e) => handleRespuestaChange('cme', e.target.value)}
className="w-full"
placeholder="Ej: 30.00"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Costo Total ($)
</label>
<Input
type="number"
value={respuestas.ct}
onChange={(e) => handleRespuestaChange('ct', e.target.value)}
className="w-full"
placeholder="CMe × Q"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de escala (economias/constante/diseconomias)
</label>
<Input
type="text"
value={respuestas.rango}
onChange={(e) => handleRespuestaChange('rango', e.target.value)}
className="w-full"
placeholder="economias"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuestas
</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">¡Correcto! Observa cómo el CMe cambia según la escala</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">Revisa tus respuestas:</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-red-50 border-red-200">
<h4 className="font-semibold text-red-900 mb-2">Causas de las Diseconomías de Escala:</h4>
<ul className="space-y-1 text-sm text-red-800">
<li> <strong>Problemas de coordinación:</strong> Más difícil coordinar muchos departamentos</li>
<li> <strong>Burocracia:</strong> Decisiones lentas y procesos administrativos complejos</li>
<li> <strong>Problemas de comunicación:</strong> Información se distorsiona en cadenas largas</li>
<li> <strong>Desmotivación:</strong> Trabajadores se sienten insignificantes en empresas grandes</li>
</ul>
</Card>
</div>
);
}
export default DiseconomiasEscala;