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