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

310 lines
12 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, 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;