Files
econ/frontend/src/components/exercises/modulo1/EscasezSimulator.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

190 lines
6.7 KiB
TypeScript

import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, AlertCircle } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const NECESIDADES = [
{ id: 'alimentacion', nombre: 'Alimentación', icono: '🍽️' },
{ id: 'vivienda', nombre: 'Vivienda', icono: '🏠' },
{ id: 'educacion', nombre: 'Educación', icono: '📚' },
{ id: 'salud', nombre: 'Salud', icono: '🏥' }
];
export function EscasezSimulator({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [asignaciones, setAsignaciones] = useState<Record<string, number>>({
alimentacion: 25,
vivienda: 25,
educacion: 25,
salud: 25
});
const [validado, setValidado] = useState(false);
const [completado, setCompletado] = useState(false);
const total = Object.values(asignaciones).reduce((sum, val) => sum + val, 0);
const restante = 100 - total;
const excedido = total > 100;
const handleSliderChange = (id: string, value: number) => {
setAsignaciones(prev => ({ ...prev, [id]: value }));
setValidado(false);
};
const handleValidar = () => {
if (excedido) return;
setValidado(true);
// Calcular puntuación basada en equilibrio
// Ideal: todas las necesidades tienen al menos 15 puntos y no se excede
const valores = Object.values(asignaciones);
const todasConMinimo = valores.every(v => v >= 15);
const sumaExacta = total === 100;
let puntuacion = 0;
if (sumaExacta) {
puntuacion = 60; // Base por usar exactamente 100
if (todasConMinimo) puntuacion += 40; // Bonus por equilibrio
} else if (total <= 100) {
puntuacion = Math.round((total / 100) * 50); // Proporcional si no usa todo
}
setTimeout(() => {
setCompletado(true);
onComplete?.(puntuacion);
}, 1500);
};
if (completado) {
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Simulación Completada!</h3>
<p className="text-gray-600 mb-6">
Has distribuido los recursos disponibles.
</p>
<div className="bg-gray-50 rounded-xl p-4 mb-6">
<p className="text-sm text-gray-600 mb-2">Distribución final:</p>
<div className="grid grid-cols-2 gap-2 text-sm">
{NECESIDADES.map(nec => (
<div key={nec.id} className="flex justify-between">
<span>{nec.icono} {nec.nombre}:</span>
<span className="font-bold">{asignaciones[nec.id]} pts</span>
</div>
))}
</div>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
<div className="text-center mb-6">
<h3 className="text-xl font-bold text-gray-900 mb-2">Simulador de Escasez</h3>
<p className="text-gray-600">
Tienes <span className="font-bold text-blue-600">100 puntos</span> para distribuir entre 4 necesidades básicas.
</p>
</div>
{/* Indicador de recursos */}
<div className="mb-6">
<div className="flex justify-between text-sm mb-2">
<span className={excedido ? 'text-red-600 font-bold' : 'text-gray-600'}>
{excedido ? '¡Excedido!' : `Restante: ${restante} pts`}
</span>
<span className={`font-bold ${excedido ? 'text-red-600' : total === 100 ? 'text-green-600' : 'text-blue-600'}`}>
{total} / 100
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<motion.div
className={`h-3 rounded-full transition-all ${
excedido ? 'bg-red-500' : total === 100 ? 'bg-green-500' : 'bg-blue-500'
}`}
animate={{ width: `${Math.min(total, 100)}%` }}
/>
</div>
{excedido && (
<p className="text-red-600 text-sm mt-2 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
Has excedido los 100 puntos disponibles. Reduce alguna asignación.
</p>
)}
</div>
{/* Sliders */}
<div className="space-y-6 mb-6">
{NECESIDADES.map(necesidad => (
<div key={necesidad.id} className="bg-gray-50 p-4 rounded-xl">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-2xl">{necesidad.icono}</span>
<span className="font-medium text-gray-700">{necesidad.nombre}</span>
</div>
<span className="text-lg font-bold text-blue-600">
{asignaciones[necesidad.id]} pts
</span>
</div>
<input
type="range"
min="0"
max="50"
value={asignaciones[necesidad.id]}
onChange={(e) => handleSliderChange(necesidad.id, parseInt(e.target.value))}
className="w-full h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
<div className="flex justify-between text-xs text-gray-400 mt-1">
<span>0</span>
<span>50</span>
</div>
</div>
))}
</div>
{/* Botón validar */}
<Button
onClick={handleValidar}
disabled={excedido}
className="w-full"
variant={excedido ? 'outline' : 'primary'}
>
{excedido ? 'Ajusta las asignaciones' : 'Validar Distribución'}
</Button>
{validado && !excedido && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-xl text-center"
>
<p className="text-blue-800">
{total === 100
? '¡Excelente! Has utilizado todos los recursos disponibles.'
: `Has utilizado ${total} de 100 puntos. ¿Quieres ajustar o continuar?`}
</p>
</motion.div>
)}
</div>
</Card>
);
}
export default EscasezSimulator;