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,213 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, Clock, Calendar, AlertCircle } from 'lucide-react';
import { QuizExercise, QuizOption } from '../common/QuizExercise';
interface CortoVsLargoPlazoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FactorItem {
id: string;
nombre: string;
tipo: 'fijo' | 'variable';
descripcion: string;
}
const factores: FactorItem[] = [
{ id: '1', nombre: 'Edificio de fábrica', tipo: 'fijo', descripcion: 'No se puede cambiar en el corto plazo' },
{ id: '2', nombre: 'Maquinaria especializada', tipo: 'fijo', descripcion: 'Requiere tiempo para adquirir o vender' },
{ id: '3', nombre: 'Trabajadores temporales', tipo: 'variable', descripcion: 'Se pueden contratar/despedir rápidamente' },
{ id: '4', nombre: 'Materias primas', tipo: 'variable', descripcion: 'Se ajustan según la producción' },
{ id: '5', nombre: 'Contrato de arrendamiento', tipo: 'fijo', descripcion: 'Compromiso a largo plazo' },
{ id: '6', nombre: 'Horas extras', tipo: 'variable', descripcion: 'Se pueden aumentar o disminuir' },
];
export function CortoVsLargoPlazo({ ejercicioId: _ejercicioId, onComplete }: CortoVsLargoPlazoProps) {
const [asignaciones, setAsignaciones] = useState<Record<string, 'fijo' | 'variable' | null>>({});
const [showResults, setShowResults] = useState(false);
const [puntuacion, setPuntuacion] = useState(0);
const handleAsignar = (id: string, tipo: 'fijo' | 'variable') => {
if (showResults) return;
setAsignaciones(prev => ({ ...prev, [id]: tipo }));
};
const handleVerificar = () => {
let correctas = 0;
factores.forEach(factor => {
if (asignaciones[factor.id] === factor.tipo) {
correctas++;
}
});
const puntaje = Math.round((correctas / factores.length) * 100);
setPuntuacion(puntaje);
setShowResults(true);
if (onComplete && puntaje >= 70) {
onComplete(puntaje);
}
};
const handleReiniciar = () => {
setAsignaciones({});
setShowResults(false);
setPuntuacion(0);
};
const todasAsignadas = factores.every(f => asignaciones[f.id] !== undefined);
const quizOptions: QuizOption[] = [
{ id: 'a', text: 'En el corto plazo todos los factores son variables', isCorrect: false },
{ id: 'b', text: 'En el corto plazo al menos un factor es fijo', isCorrect: true },
{ id: 'c', text: 'En el largo plazo no hay factores variables', isCorrect: false },
{ id: 'd', text: 'El tiempo determina si un factor es fijo o variable', isCorrect: false },
];
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Corto Plazo vs Largo Plazo"
subtitle="Clasifica los factores de producción según su variabilidad"
/>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="bg-orange-50 p-4 rounded-lg border border-orange-200">
<div className="flex items-center gap-2 mb-2">
<Clock className="w-5 h-5 text-orange-600" />
<h4 className="font-semibold text-orange-800">Corto Plazo</h4>
</div>
<p className="text-sm text-orange-700">
Periodo en el que al menos un factor de producción es <strong>fijo</strong>.
No se puede cambiar la cantidad de todos los factores.
</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<div className="flex items-center gap-2 mb-2">
<Calendar className="w-5 h-5 text-green-600" />
<h4 className="font-semibold text-green-800">Largo Plazo</h4>
</div>
<p className="text-sm text-green-700">
Periodo en el que <strong>todos los factores son variables</strong>.
La empresa puede ajustar todas sus capacidades productivas.
</p>
</div>
</div>
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-4">
Clasifica cada factor como Fijo o Variable en el corto plazo:
</h4>
<div className="space-y-3">
{factores.map((factor) => (
<div
key={factor.id}
className={`p-4 rounded-lg border-2 transition-all ${
showResults
? asignaciones[factor.id] === factor.tipo
? 'border-success bg-success/10'
: 'border-error bg-error/10'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">{factor.nombre}</p>
<p className="text-sm text-gray-500">{factor.descripcion}</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant={asignaciones[factor.id] === 'fijo' ? 'primary' : 'outline'}
onClick={() => handleAsignar(factor.id, 'fijo')}
disabled={showResults}
>
Fijo
</Button>
<Button
size="sm"
variant={asignaciones[factor.id] === 'variable' ? 'primary' : 'outline'}
onClick={() => handleAsignar(factor.id, 'variable')}
disabled={showResults}
>
Variable
</Button>
</div>
</div>
{showResults && (
<p className={`text-sm mt-2 ${
asignaciones[factor.id] === factor.tipo ? 'text-success' : 'text-error'
}`}>
{asignaciones[factor.id] === factor.tipo
? '✓ Correcto'
: `✗ Incorrecto. Es un factor ${factor.tipo}`}
</p>
)}
</div>
))}
</div>
</div>
{!showResults ? (
<div className="flex justify-end">
<Button
onClick={handleVerificar}
disabled={!todasAsignadas}
size="lg"
>
Verificar Respuestas
</Button>
</div>
) : (
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-gray-900">
Puntuación: {puntuacion}%
</p>
<p className="text-sm text-gray-600">
{puntuacion >= 70
? '¡Buen trabajo! Has comprendido la diferencia entre factores fijos y variables.'
: 'Repasa los conceptos e intenta de nuevo.'}
</p>
</div>
<Button onClick={handleReiniciar} variant="outline">
Reiniciar
</Button>
</div>
</div>
)}
</Card>
<QuizExercise
question="¿Cuál es la característica distintiva del corto plazo en la teoría de la producción?"
options={quizOptions}
explanation="En el corto plazo, al menos un factor de producción es fijo (generalmente el capital o la planta), mientras que otros factores como el trabajo pueden variar. Esto contrasta con el largo plazo donde todos los factores son variables."
onComplete={(result) => {
if (result.correct && onComplete && puntuacion >= 70) {
onComplete(Math.max(puntuacion, result.score));
}
}}
exerciseId="corto-largo-plazo-quiz"
/>
<div className="flex justify-end">
<Button
onClick={() => onComplete?.(Math.max(puntuacion, 100))}
size="lg"
disabled={puntuacion < 70}
>
<CheckCircle className="w-5 h-5 mr-2" />
Completar Ejercicio
</Button>
</div>
</div>
);
}
export default CortoVsLargoPlazo;

View File

@@ -0,0 +1,212 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Calculator, CheckCircle, XCircle } from 'lucide-react';
export function CostoTotalMedioMarginal() {
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({
cme_q2: '',
cme_q4: '',
cmg_q3: '',
cmg_q5: '',
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const datos = [
{ q: 0, ct: 100 },
{ q: 1, ct: 150 },
{ q: 2, ct: 180 },
{ q: 3, ct: 220 },
{ q: 4, ct: 300 },
{ q: 5, ct: 450 },
];
// Cálculos correctos
const respuestasCorrectas: { [key: string]: string } = {
cme_q2: '90', // 180/2
cme_q4: '75', // 300/4
cmg_q3: '40', // 220-180
cmg_q5: '150', // 450-300
};
const handleInputChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasCompletadas = Object.values(respuestas).every(r => r !== '');
const esCorrecto = (campo: string) => {
return respuestas[campo] === respuestasCorrectas[campo];
};
const correctas = Object.keys(respuestasCorrectas).filter(esCorrecto).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Costo Total, Medio y Marginal"
subtitle="Calcula CMe (CT/Q) y CMg (ΔCT/ΔQ) a partir de los datos"
/>
<div className="space-y-6">
{/* Datos base */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-4 py-2 text-left font-medium text-gray-700">Cantidad (Q)</th>
<th className="px-4 py-2 text-left font-medium text-gray-700">Costo Total (CT)</th>
<th className="px-4 py-2 text-left font-medium text-gray-700">CF (100)</th>
<th className="px-4 py-2 text-left font-medium text-gray-700">CV</th>
</tr>
</thead>
<tbody>
{datos.map((fila) => (
<tr key={fila.q} className="border-b">
<td className="px-4 py-2 font-medium">{fila.q}</td>
<td className="px-4 py-2">${fila.ct}</td>
<td className="px-4 py-2 text-gray-600">$100</td>
<td className="px-4 py-2 text-gray-600">${fila.ct - 100}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Preguntas */}
<div className="bg-amber-50 p-4 rounded-lg border border-amber-200">
<h4 className="font-semibold text-amber-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5" />
Calcula los siguientes valores:
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">1. CMe cuando Q = 2</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT / Q = 180 / 2</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cme_q2}
onChange={(e) => handleInputChange('cme_q2', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cme_q2')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cme_q2')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cme_q2') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cme_q2') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">2. CMe cuando Q = 4</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT / Q = 300 / 4</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cme_q4}
onChange={(e) => handleInputChange('cme_q4', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cme_q4')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cme_q4')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cme_q4') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cme_q4') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">3. CMg del 2do al 3er trabajador</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT - CT = 220 - 180</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cmg_q3}
onChange={(e) => handleInputChange('cmg_q3', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cmg_q3')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cmg_q3')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cmg_q3') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cmg_q3') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">4. CMg del 4to al 5to trabajador</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT - CT = 450 - 300</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cmg_q5}
onChange={(e) => handleInputChange('cmg_q5', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cmg_q5')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cmg_q5')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cmg_q5') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cmg_q5') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
</div>
</div>
<Button onClick={validar} disabled={!todasCompletadas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 4 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<p className="font-semibold">Resultado: {correctas}/4 correctas</p>
{correctas < 4 && (
<p className="text-sm mt-2">Las respuestas correctas son: CMe(Q=2)=$90, CMe(Q=4)=$75, CMg(23)=$40, CMg(45)=$150</p>
)}
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas Importantes</h4>
<div className="space-y-1 text-sm text-blue-800">
<p><strong>Costo Medio (CMe):</strong> CMe = CT / Q</p>
<p><strong>Costo Marginal (CMg):</strong> CMg = ΔCT / ΔQ = CTₙ - CTₙ</p>
<p className="mt-2 text-blue-700">Observa cómo el CMg aumenta significativamente del 4to al 5to trabajador ($150 vs $40),
mostrando los rendimientos decrecientes.</p>
</div>
</Card>
</div>
);
}
export default CostoTotalMedioMarginal;

View File

@@ -0,0 +1,180 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, DollarSign } from 'lucide-react';
export function CostosFijosVsVariables() {
const [clasificaciones, setClasificaciones] = useState<{[key: string]: 'fijo' | 'variable' | null}>({
alquiler: null,
materias: null,
salarios: null,
luz: null,
depreciacion: null,
publicidad: null,
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const conceptos = [
{ id: 'alquiler', nombre: 'Alquiler del local', tipo: 'fijo' as const, explicacion: 'El alquiler se paga mensualmente independientemente de cuánto produzcas.' },
{ id: 'materias', nombre: 'Materias primas', tipo: 'variable' as const, explicacion: 'A más producción, más materias primas necesitas.' },
{ id: 'salarios', nombre: 'Salarios de obreros temporales', tipo: 'variable' as const, explicacion: 'Los obreros temporales se contratan según la demanda de producción.' },
{ id: 'luz', nombre: 'Electricidad de máquinas', tipo: 'variable' as const, explicacion: 'Más horas de producción = más consumo eléctrico.' },
{ id: 'depreciacion', nombre: 'Depreciación de maquinaria', tipo: 'fijo' as const, explicacion: 'La depreciación ocurre con el paso del tiempo, no con la cantidad producida.' },
{ id: 'publicidad', nombre: 'Publicidad (contrato anual)', tipo: 'fijo' as const, explicacion: 'El contrato de publicidad es un costo fijo por período.' },
];
const clasificar = (id: string, tipo: 'fijo' | 'variable') => {
setClasificaciones(prev => ({ ...prev, [id]: tipo }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasClasificadas = Object.values(clasificaciones).every(c => c !== null);
const correctas = conceptos.filter(c => clasificaciones[c.id] === c.tipo).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Costos Fijos vs Variables"
subtitle="Clasifica cada costo como FIJO (no varía con la producción) o VARIABLE (cambia con la producción)"
/>
<div className="space-y-6">
{/* Gráfico comparativo */}
<div className="bg-gray-50 rounded-lg p-4">
<svg className="w-full h-48" viewBox="0 0 600 180">
{/* Título */}
<text x="300" y="20" textAnchor="middle" className="text-base font-bold fill-gray-800">Comportamiento de Costos Fijos y Variables</text>
{/* Gráfico CF */}
<text x="80" y="45" textAnchor="middle" className="text-sm font-bold fill-blue-700">Costo Fijo (CF)</text>
<line x1="30" y1="150" x2="160" y2="150" stroke="#374151" strokeWidth="2" />
<line x1="30" y1="150" x2="30" y2="50" stroke="#374151" strokeWidth="2" />
<text x="100" y="165" textAnchor="middle" className="text-xs fill-gray-600">Q</text>
<text x="15" y="100" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 15 100)">$</text>
{/* Línea horizontal CF */}
<line x1="30" y1="80" x2="150" y2="80" stroke="#2563eb" strokeWidth="3" />
<text x="95" y="70" textAnchor="middle" className="text-xs fill-blue-600">CF = 1000</text>
{/* Gráfico CV */}
<text x="280" y="45" textAnchor="middle" className="text-sm font-bold fill-green-700">Costo Variable (CV)</text>
<line x1="200" y1="150" x2="330" y2="150" stroke="#374151" strokeWidth="2" />
<line x1="200" y1="150" x2="200" y2="50" stroke="#374151" strokeWidth="2" />
<text x="270" y="165" textAnchor="middle" className="text-xs fill-gray-600">Q</text>
<text x="185" y="100" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 185 100)">$</text>
{/* Línea creciente CV */}
<path d="M 200,150 L 220,130 L 250,100 L 290,60 L 320,30" fill="none" stroke="#16a34a" strokeWidth="3" />
{/* Gráfico CT */}
<text x="480" y="45" textAnchor="middle" className="text-sm font-bold fill-purple-700">Costo Total (CT)</text>
<line x1="370" y1="150" x2="550" y2="150" stroke="#374151" strokeWidth="2" />
<line x1="370" y1="150" x2="370" y2="50" stroke="#374151" strokeWidth="2" />
<text x="480" y="165" textAnchor="middle" className="text-xs fill-gray-600">Q</text>
<text x="355" y="100" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 355 100)">$</text>
{/* Línea CT = CF + CV */}
<path d="M 370,80 L 390,70 L 420,55 L 460,40 L 520,30" fill="none" stroke="#9333ea" strokeWidth="3" />
{/* Línea punteada CF */}
<line x1="370" y1="80" x2="550" y2="80" stroke="#2563eb" strokeWidth="1" strokeDasharray="4" opacity="0.5" />
<text x="540" y="75" textAnchor="end" className="text-xs fill-blue-600">CF</text>
</svg>
</div>
{/* Ejercicio de clasificación */}
<div className="space-y-3">
{conceptos.map((concepto) => {
const esCorrecto = mostrarResultados && clasificaciones[concepto.id] === concepto.tipo;
const esIncorrecto = mostrarResultados && clasificaciones[concepto.id] !== concepto.tipo && clasificaciones[concepto.id] !== null;
return (
<div key={concepto.id} className="bg-white border rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<DollarSign className="w-5 h-5 text-gray-500" />
<span className="font-medium text-gray-900">{concepto.nombre}</span>
</div>
<div className="flex gap-2">
<button
onClick={() => clasificar(concepto.id, 'fijo')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg border-2 font-medium transition-all ${
clasificaciones[concepto.id] === 'fijo' && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && concepto.tipo === 'fijo'
? 'border-green-500 bg-green-50'
: mostrarResultados && clasificaciones[concepto.id] === 'fijo' && concepto.tipo !== 'fijo'
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
FIJO
{mostrarResultados && concepto.tipo === 'fijo' && (
<CheckCircle className="w-4 h-4 text-green-600 inline ml-1" />
)}
</button>
<button
onClick={() => clasificar(concepto.id, 'variable')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg border-2 font-medium transition-all ${
clasificaciones[concepto.id] === 'variable' && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && concepto.tipo === 'variable'
? 'border-green-500 bg-green-50'
: mostrarResultados && clasificaciones[concepto.id] === 'variable' && concepto.tipo !== 'variable'
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
VARIABLE
{mostrarResultados && concepto.tipo === 'variable' && (
<CheckCircle className="w-4 h-4 text-green-600 inline ml-1" />
)}
</button>
</div>
</div>
{mostrarResultados && (
<div className={`mt-3 p-2 rounded text-sm ${esCorrecto ? 'bg-green-50 text-green-800' : 'bg-amber-50 text-amber-800'}`}>
<strong>{concepto.tipo === 'fijo' ? 'FIJO' : 'VARIABLE'}:</strong> {concepto.explicacion}
</div>
)}
</div>
);
})}
</div>
<Button onClick={validar} disabled={!todasClasificadas || mostrarResultados}>
Validar Clasificación
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 6 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
{correctas === 6 ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<XCircle className="w-5 h-5 text-amber-600" />
)}
<span className="font-semibold">Resultado: {correctas}/6 correctas</span>
</div>
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Definiciones Clave</h4>
<div className="space-y-2 text-sm">
<p><strong className="text-blue-800">Costo Fijo (CF):</strong> <span className="text-blue-700">No depende del nivel de producción. Se incurren aunque Q = 0.</span></p>
<p><strong className="text-blue-800">Costo Variable (CV):</strong> <span className="text-blue-700">Varía directamente con la cantidad producida. CV = 0 cuando Q = 0.</span></p>
<p><strong className="text-blue-800">Costo Total (CT):</strong> <span className="text-blue-700">CT = CF + CV</span></p>
</div>
</Card>
</div>
);
}
export default CostosFijosVsVariables;

View File

@@ -0,0 +1,204 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, PieChart } from 'lucide-react';
export function CostosMedios() {
const [respuesta, setRespuesta] = useState<string | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const pregunta = {
texto: 'Según la gráfica, ¿cuál es la relación entre CFMe, CVMe y CMe en Q=4?',
opciones: [
{ id: 'a', texto: 'CFMe > CVMe > CMe', correcta: false },
{ id: 'b', texto: 'CMe = CFMe + CVMe', correcta: true },
{ id: 'c', texto: 'CVMe = CFMe + CMe', correcta: false },
{ id: 'd', texto: 'CFMe = CVMe = CMe', correcta: false },
],
explicacion: 'Correcto. El Costo Medio (CMe) es la suma del Costo Fijo Medio (CFMe) y el Costo Variable Medio (CVMe): CMe = CFMe + CVMe'
};
// Datos para la gráfica
const datos = [
{ q: 1, cfme: 100, cvme: 50, cme: 150 },
{ q: 2, cfme: 50, cvme: 40, cme: 90 },
{ q: 3, cfme: 33.33, cvme: 35, cme: 68.33 },
{ q: 4, cfme: 25, cvme: 32.5, cme: 57.5 },
{ q: 5, cfme: 20, cvme: 35, cme: 55 },
{ q: 6, cfme: 16.67, cvme: 42.5, cme: 59.17 },
];
const validar = () => {
setMostrarResultado(true);
};
const esCorrecta = respuesta === 'b';
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Costos Medios: CFMe, CVMe y CMe"
subtitle="Analiza la composición del costo medio y su relación con los costos fijos y variables"
/>
<div className="space-y-6">
{/* Gráfico de barras apiladas */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4 text-center">Descomposición del Costo Medio</h4>
<svg className="w-full h-72" viewBox="0 0 600 250">
{/* Ejes */}
<line x1="60" y1="220" x2="550" y2="220" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="220" x2="60" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="305" y="245" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
<text x="25" y="120" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 25 120)">Costo Medio ($)</text>
{/* Barras apiladas */}
{datos.map((d, i) => {
const x = 90 + i * 80;
const alturaCVMe = (d.cvme / 160) * 180;
const alturaCFMe = (d.cfme / 160) * 180;
const alturaTotal = alturaCVMe + alturaCFMe;
return (
<g key={i}>
{/* CFMe (parte superior) */}
<rect
x={x - 25}
y={220 - alturaTotal}
width="50"
height={alturaCFMe}
fill="#3b82f6"
stroke="white"
strokeWidth="1"
/>
{/* CVMe (parte inferior) */}
<rect
x={x - 25}
y={220 - alturaCVMe}
width="50"
height={alturaCVMe}
fill="#22c55e"
stroke="white"
strokeWidth="1"
/>
{/* Etiqueta Q */}
<text x={x} y="235" textAnchor="middle" className="text-sm fill-gray-700 font-bold">{d.q}</text>
{/* Valor CMe */}
<text x={x} y={220 - alturaTotal - 5} textAnchor="middle" className="text-xs fill-gray-700 font-bold">
${d.cme.toFixed(1)}
</text>
</g>
);
})}
{/* Línea de CMe */}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="3"
points={datos.map((d, i) => {
const x = 90 + i * 80;
const alturaTotal = ((d.cvme + d.cfme) / 160) * 180;
return `${x},${220 - alturaTotal}`;
}).join(' ')}
/>
{/* Leyenda */}
<g transform="translate(420, 40)">
<rect x="0" y="0" width="15" height="15" fill="#3b82f6" />
<text x="20" y="12" className="text-xs fill-gray-700">CFMe</text>
<rect x="0" y="25" width="15" height="15" fill="#22c55e" />
<text x="20" y="37" className="text-xs fill-gray-700">CVMe</text>
<line x1="0" y1="55" x2="20" y2="55" stroke="#7c3aed" strokeWidth="3" />
<text x="25" y="59" className="text-xs fill-gray-700">CMe = CFMe + CVMe</text>
</g>
</svg>
</div>
{/* Observaciones */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h5 className="font-semibold text-blue-900 mb-2">CFMe (Costo Fijo Medio)</h5>
<p className="text-sm text-blue-800">CFMe = CF / Q</p>
<p className="text-sm text-blue-700 mt-1">Siempre <strong>decreciente</strong>. A mayor producción, el costo fijo se "reparte" entre más unidades.</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<h5 className="font-semibold text-green-900 mb-2">CVMe (Costo Variable Medio)</h5>
<p className="text-sm text-green-800">CVMe = CV / Q</p>
<p className="text-sm text-green-700 mt-1">Tiene forma de <strong>U</strong>. Primero baja por eficiencias, luego sube por rendimientos decrecientes.</p>
</div>
</div>
{/* Pregunta */}
<div className="bg-white border rounded-lg p-4">
<h4 className="font-semibold text-gray-900 mb-3">{pregunta.texto}</h4>
<div className="space-y-2">
{pregunta.opciones.map((opcion) => (
<button
key={opcion.id}
onClick={() => {
setRespuesta(opcion.id);
setMostrarResultado(false);
}}
disabled={mostrarResultado}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === opcion.id && !mostrarResultado
? 'border-blue-500 bg-blue-50'
: mostrarResultado && opcion.correcta
? 'border-green-500 bg-green-50'
: mostrarResultado && respuesta === opcion.id && !opcion.correcta
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">{opcion.id})</span> {opcion.texto}
{mostrarResultado && opcion.correcta && <CheckCircle className="w-5 h-5 text-green-600 inline ml-2" />}
{mostrarResultado && respuesta === opcion.id && !opcion.correcta && <XCircle className="w-5 h-5 text-red-600 inline ml-2" />}
</button>
))}
</div>
<Button onClick={validar} disabled={!respuesta || mostrarResultado} className="mt-4">
Validar Respuesta
</Button>
{mostrarResultado && (
<div className={`mt-4 p-4 rounded-lg border ${esCorrecta ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-center gap-2 mb-2">
{esCorrecta ? <CheckCircle className="w-5 h-5 text-green-600" /> : <XCircle className="w-5 h-5 text-red-600" />}
<span className={`font-semibold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className={`text-sm ${esCorrecta ? 'text-green-700' : 'text-red-700'}`}>{pregunta.explicacion}</p>
</div>
)}
</div>
</div>
</Card>
<Card className="bg-purple-50 border-purple-200">
<div className="flex items-center gap-2 mb-2">
<PieChart className="w-5 h-5 text-purple-600" />
<h4 className="font-semibold text-purple-900">Resumen de Fórmulas</h4>
</div>
<div className="space-y-1 text-sm text-purple-800">
<p><strong>CFMe</strong> = CF / Q (siempre decreciente)</p>
<p><strong>CVMe</strong> = CV / Q (forma de U)</p>
<p><strong>CMe</strong> = CFMe + CVMe = CT / Q (forma de U)</p>
<p className="mt-2 text-purple-700">Observa cómo CFMe se vuelve insignificante a altos niveles de producción,
mientras que CVMe domina el costo medio.</p>
</div>
</Card>
</div>
);
}
export default CostosMedios;

View File

@@ -0,0 +1,274 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, TrendingUp, RotateCcw, Calculator } from 'lucide-react';
interface CurvaCostoLargoPlazoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface DatosEscala {
q: number;
cme: number;
}
export function CurvaCostoLargoPlazo({ ejercicioId: _ejercicioId, onComplete }: CurvaCostoLargoPlazoProps) {
const datosBase: DatosEscala[] = [
{ q: 1, cme: 120 },
{ q: 2, cme: 85 },
{ q: 3, cme: 70 },
{ q: 4, cme: 65 },
{ q: 5, cme: 62 },
{ q: 6, cme: 60 },
{ q: 7, cme: 61 },
{ q: 8, cme: 64 },
{ q: 9, cme: 69 },
{ q: 10, cme: 75 },
];
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({
cmeMinimo: '',
cantidadOptima: '',
ctQ5: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados = useMemo(() => {
return datosBase.map(d => ({
...d,
ct: d.q * d.cme,
}));
}, []);
const cmeMinimo = useMemo(() => {
return Math.min(...datosBase.map(d => d.cme));
}, []);
const cantidadOptima = useMemo(() => {
const minCME = Math.min(...datosBase.map(d => d.cme));
return datosBase.find(d => d.cme === minCME)?.q || 0;
}, []);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseFloat(respuestas.cmeMinimo) !== cmeMinimo) {
nuevosErrores.push('El CMe mínimo no es correcto. Observa la curva U.');
}
if (parseFloat(respuestas.cantidadOptima) !== cantidadOptima) {
nuevosErrores.push('La cantidad óptima no es correcta. Es donde el CMe es mínimo.');
}
if (parseFloat(respuestas.ctQ5) !== 310) {
nuevosErrores.push('El CT a Q=5 es incorrecto. Recuerda: CT = CMe × Q');
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({ cmeMinimo: '', cantidadOptima: '', ctQ5: '' });
setValidado(false);
setErrores([]);
};
const maxCMe = Math.max(...datosBase.map(d => d.cme));
const escalaY = 120 / maxCMe;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Curva de Costo Largo Plazo (CMe LP)"
subtitle="La curva en U del costo medio a largo plazo"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Concepto</span>
</div>
<p className="text-sm text-blue-700">
A largo plazo todos los factores son variables. La curva CMeLP tiene forma de U
debido a las economías y deseconomías de escala. El punto mínimo representa la
escala eficiente de producción.
</p>
</div>
<div className="h-64 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 200">
<line x1="40" y1="180" x2="380" y2="180" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="180" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="195" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="100" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 100)">CMe ($)</text>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((q, i) => (
<g key={q}>
<line x1={60 + i * 30} y1="180" x2={60 + i * 30} y2="185" stroke="#374151" strokeWidth="1" />
<text x={60 + i * 30} y="195" textAnchor="middle" className="text-xs fill-gray-500">{q}</text>
</g>
))}
{[20, 40, 60, 80, 100, 120].map((val, i) => (
<g key={val}>
<line x1="35" y1={180 - val * escalaY} x2="40" y2={180 - val * escalaY} stroke="#374151" strokeWidth="1" />
<text x="30" y={185 - val * escalaY} textAnchor="end" className="text-xs fill-gray-500">{val}</text>
</g>
))}
<path
d={`M ${60},${180 - datosBase[0].cme * escalaY} ${datosBase.slice(1).map((d, i) => `L ${60 + (i + 1) * 30},${180 - d.cme * escalaY}`).join(' ')}`}
fill="none"
stroke="#7c3aed"
strokeWidth="3"
/>
{datosBase.map((d, i) => (
<circle
key={i}
cx={60 + i * 30}
cy={180 - d.cme * escalaY}
r="5"
fill="#7c3aed"
stroke="white"
strokeWidth="2"
/>
))}
<circle
cx={60 + 5 * 30}
cy={180 - 60 * escalaY}
r="8"
fill="#10b981"
stroke="white"
strokeWidth="3"
/>
<text x={210} y={170 - 60 * escalaY} textAnchor="middle" className="text-xs fill-green-600 font-bold">
Mínimo CMe = $60
</text>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CMe ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT ($)</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((d, i) => (
<tr key={i} className={`border-b hover:bg-gray-50 ${d.cme === cmeMinimo ? 'bg-green-50' : ''}`}>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2 font-medium text-primary">{d.cme}</td>
<td className="px-3 py-2 text-gray-600">{d.ct}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5 text-purple-600" />
Responde las siguientes preguntas:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cuál es el CMe mínimo? ($)
</label>
<Input
type="number"
value={respuestas.cmeMinimo}
onChange={(e) => handleRespuestaChange('cmeMinimo', e.target.value)}
className="w-full"
placeholder="Ej: 60"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿A qué cantidad ocurre? (Q)
</label>
<Input
type="number"
value={respuestas.cantidadOptima}
onChange={(e) => handleRespuestaChange('cantidadOptima', e.target.value)}
className="w-full"
placeholder="Ej: 6"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿CT cuando Q = 5? ($)
</label>
<Input
type="number"
value={respuestas.ctQ5}
onChange={(e) => handleRespuestaChange('ctQ5', e.target.value)}
className="w-full"
placeholder="CMe × Q"
/>
</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! La escala eficiente es Q = 6 con CMe = $60</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-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas importantes:</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li><strong>CT</strong> = CMe × Q (Costo Total)</li>
<li><strong>CMe LP</strong> = Costo medio a largo plazo (todas las plantas posibles)</li>
<li><strong>Escala eficiente</strong>: Cantidad donde CMe es mínimo</li>
</ul>
</Card>
</div>
);
}
export default CurvaCostoLargoPlazo;

View File

@@ -0,0 +1,218 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { TrendingUp, CheckCircle, DollarSign } from 'lucide-react';
export function CurvasCosto() {
const [etapaActiva, setEtapaActiva] = useState<string | null>(null);
// Datos para las curvas
const datosCT = [
{ q: 0, ct: 100 },
{ q: 1, ct: 140 },
{ q: 2, ct: 170 },
{ q: 3, ct: 190 },
{ q: 4, ct: 220 },
{ q: 5, ct: 270 },
{ q: 6, ct: 350 },
{ q: 7, ct: 460 },
{ q: 8, ct: 600 },
];
const datosCMe = [
{ q: 1, cme: 140 },
{ q: 2, cme: 85 },
{ q: 3, cme: 63.33 },
{ q: 4, cme: 55 },
{ q: 5, cme: 54 },
{ q: 6, cme: 58.33 },
{ q: 7, cme: 65.71 },
{ q: 8, cme: 75 },
];
const datosCMg = [
{ q: 1, cmg: 40 },
{ q: 2, cmg: 30 },
{ q: 3, cmg: 20 },
{ q: 4, cmg: 30 },
{ q: 5, cmg: 50 },
{ q: 6, cmg: 80 },
{ q: 7, cmg: 110 },
{ q: 8, cmg: 140 },
];
const puntosCorte = [
{ q: 4, desc: 'CMg corta a CMe en su mínimo' },
{ q: 5, desc: 'CMe mínimo (producción eficiente)' },
];
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Curvas de Costo"
subtitle="Analiza la relación entre CT, CMe y CMg con gráficos interactivos"
/>
<div className="space-y-6">
{/* Gráfico de Costo Total */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700">Curva de Costo Total (CT)</h4>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">CT = CF + CV</span>
</div>
<svg className="w-full h-56" viewBox="0 0 500 200">
{/* Ejes */}
<line x1="50" y1="170" x2="450" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="170" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="250" y="195" textAnchor="middle" className="text-xs fill-gray-600">Cantidad (Q)</text>
<text x="20" y="95" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 20 95)">Costo Total ($)</text>
{/* CF horizontal */}
<line x1="50" y1="130" x2="450" y2="130" stroke="#2563eb" strokeWidth="2" strokeDasharray="4" />
<text x="430" y="125" textAnchor="end" className="text-xs fill-blue-600">CF = 100</text>
{/* Curva CT */}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="3"
points={datosCT.map((d, i) => `${50 + i * 45},${170 - (d.ct / 700) * 150}`).join(' ')}
/>
{/* Puntos */}
{datosCT.map((d, i) => (
<circle
key={i}
cx={50 + i * 45}
cy={170 - (d.ct / 700) * 150}
r="4"
fill="#7c3aed"
/>
))}
{/* Etiquetas de Q */}
{datosCT.map((d, i) => (
<text key={i} x={50 + i * 45} y="185" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
</svg>
</div>
{/* Gráfico de CMe y CMg */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700">Curvas de CMe y CMg</h4>
<div className="flex gap-2">
<span className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded flex items-center gap-1">
<span className="w-3 h-0.5 bg-purple-600"></span> CMe
</span>
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded flex items-center gap-1">
<span className="w-3 h-0.5 bg-green-600 border-dashed"></span> CMg
</span>
</div>
</div>
<svg className="w-full h-56" viewBox="0 0 500 200">
{/* Ejes */}
<line x1="50" y1="170" x2="450" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="170" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="250" y="195" textAnchor="middle" className="text-xs fill-gray-600">Cantidad (Q)</text>
<text x="20" y="95" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 20 95)">Costo ($)</text>
{/* Curva CMe */}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="3"
points={datosCMe.map((d, i) => `${95 + i * 45},${170 - (d.cme / 160) * 150}`).join(' ')}
/>
{/* Curva CMg */}
<polyline
fill="none"
stroke="#16a34a"
strokeWidth="3"
strokeDasharray="5"
points={datosCMg.map((d, i) => `${95 + i * 45},${170 - (d.cmg / 160) * 150}`).join(' ')}
/>
{/* Puntos de corte */}
<circle cx="275" cy="127" r="6" fill="#ef4444" stroke="white" strokeWidth="2" />
<text x="290" y="120" className="text-xs fill-red-600 font-bold">Mínimo CMe</text>
{/* Etiquetas de Q */}
{datosCMe.map((d, i) => (
<text key={i} x={95 + i * 45} y="185" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
{/* Leyenda */}
<g transform="translate(350, 40)">
<line x1="0" y1="0" x2="30" y2="0" stroke="#7c3aed" strokeWidth="2" />
<text x="35" y="4" className="text-xs fill-gray-700">CMe</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#16a34a" strokeWidth="2" strokeDasharray="4" />
<text x="35" y="24" className="text-xs fill-gray-700">CMg</text>
</g>
</svg>
</div>
{/* Puntos clave */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{puntosCorte.map((punto, index) => (
<button
key={index}
onClick={() => setEtapaActiva(etapaActiva === `punto-${index}` ? null : `punto-${index}`)}
className={`p-4 rounded-lg border-2 text-left transition-all ${
etapaActiva === `punto-${index}`
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-center gap-2 mb-2">
<div className="w-4 h-4 rounded-full bg-red-500"></div>
<span className="font-semibold text-gray-900">Q = {punto.q}</span>
</div>
<p className="text-sm text-gray-600">{punto.desc}</p>
</button>
))}
</div>
{etapaActiva && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-900">Análisis</span>
</div>
<p className="text-sm text-blue-800">
En Q=5 se alcanza el <strong>CMe mínimo</strong> ($54), que es el punto donde CMg = CMe.
Este es el nivel de producción más eficiente en términos de costos medios.
</p>
</div>
)}
</div>
</Card>
<Card className="bg-amber-50 border-amber-200">
<div className="flex items-center gap-2 mb-2">
<DollarSign className="w-5 h-5 text-amber-600" />
<h4 className="font-semibold text-amber-900">Interpretación Económica</h4>
</div>
<ul className="space-y-2 text-sm text-amber-800">
<li><strong>Costo Total (CT):</strong> Siempre crece porque producir más cuesta más</li>
<li><strong>Costo Medio (CMe):</strong> Tiene forma de U debido a los rendimientos decrecientes</li>
<li><strong>Costo Marginal (CMg):</strong> Corta a CMe en su punto mínimo</li>
<li><strong>Regla:</strong> Si CMg {'<'} CMe, el costo medio baja; si CMg {'>'} CMe, el costo medio sube</li>
</ul>
</Card>
</div>
);
}
export default CurvasCosto;

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;

View File

@@ -0,0 +1,217 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, ArrowDown, RotateCcw, Factory } from 'lucide-react';
interface EconomiasEscalaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface EscalaData {
planta: string;
capacidad: number;
cf: number;
cvUnitario: number;
}
export function EconomiasEscala({ ejercicioId: _ejercicioId, onComplete }: EconomiasEscalaProps) {
const datosPlantas: EscalaData[] = [
{ planta: 'Pequeña', capacidad: 100, cf: 1000, cvUnitario: 10 },
{ planta: 'Mediana', capacidad: 500, cf: 3000, cvUnitario: 8 },
{ planta: 'Grande', capacidad: 1000, cf: 5000, cvUnitario: 6 },
{ planta: 'Muy Grande', capacidad: 2000, cf: 8000, cvUnitario: 5 },
];
const [produccion, setProduccion] = useState(500);
const [seleccion, setSeleccion] = useState<string | null>(null);
const [validado, setValidado] = useState(false);
const calculos = useMemo(() => {
return datosPlantas.map(p => {
const q = Math.min(produccion, p.capacidad);
const cv = q * p.cvUnitario;
const ct = p.cf + cv;
const cme = q > 0 ? ct / q : 0;
const puedeProducir = produccion <= p.capacidad;
return { ...p, q, cv, ct, cme, puedeProducir };
});
}, [produccion]);
const plantaOptima = useMemo(() => {
const plantasFactibles = calculos.filter(c => c.puedeProducir);
if (plantasFactibles.length === 0) return null;
return plantasFactibles.reduce((min, curr) => curr.cme < min.cme ? curr : min);
}, [calculos]);
const handleValidar = () => {
setValidado(true);
if (seleccion === plantaOptima?.planta && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setProduccion(500);
setSeleccion(null);
setValidado(false);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Economías de Escala"
subtitle="Reducción del costo medio al aumentar la escala de producción"
/>
<div className="bg-green-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<ArrowDown className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-800">Concepto</span>
</div>
<p className="text-sm text-green-700">
Las economías de escala ocurren cuando el costo medio disminuye a medida que
aumenta la producción. Esto puede deberse a: especialización, tecnología eficiente,
descuentos por volumen en compras, y distribución de costos fijos.
</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">
Nivel de producción deseado (Q)
</label>
<div className="flex items-center gap-4">
<input
type="range"
min="50"
max="2000"
step="50"
value={produccion}
onChange={(e) => {
setProduccion(parseInt(e.target.value));
setValidado(false);
setSeleccion(null);
}}
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">
{produccion}
</span>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4 mb-6">
{calculos.map((calc) => (
<div
key={calc.planta}
onClick={() => calc.puedeProducir && setSeleccion(calc.planta)}
className={`
p-4 rounded-lg border-2 cursor-pointer transition-all
${!calc.puedeProducir ? 'bg-gray-100 border-gray-200 opacity-50 cursor-not-allowed' : ''}
${seleccion === calc.planta ? 'border-primary bg-primary/5' : 'border-gray-200 hover:border-gray-300'}
${validado && calc.planta === plantaOptima?.planta ? 'border-green-500 bg-green-50' : ''}
`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Factory className="w-5 h-5 text-gray-600" />
<span className="font-semibold text-gray-900">{calc.planta}</span>
</div>
{!calc.puedeProducir && (
<span className="text-xs bg-red-100 text-red-700 px-2 py-1 rounded">
Insuficiente
</span>
)}
{validado && calc.planta === plantaOptima?.planta && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded font-medium">
Óptima
</span>
)}
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Capacidad máxima:</span>
<span className="font-medium">{calc.capacidad} unidades</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Costo Fijo:</span>
<span className="font-medium">${calc.cf.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">CV unitario:</span>
<span className="font-medium">${calc.cvUnitario}</span>
</div>
{calc.puedeProducir && (
<>
<div className="flex justify-between pt-2 border-t">
<span className="text-gray-600">Costo Total:</span>
<span className="font-medium text-primary">${calc.ct.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Costo Medio:</span>
<span className={`font-bold ${calc.cme === Math.min(...calculos.filter(c => c.puedeProducir).map(c => c.cme)) ? 'text-green-600' : 'text-gray-900'}`}>
${calc.cme.toFixed(2)}
</span>
</div>
</>
)}
</div>
</div>
))}
</div>
<div className="bg-blue-50 p-4 rounded-lg mb-4">
<p className="text-sm text-blue-800 font-medium mb-2">
Selecciona la planta óptima para producir {produccion} unidades:
</p>
<p className="text-sm text-blue-600">
Tip: Elige la planta con el menor costo medio (CMe) que pueda producir la cantidad deseada.
</p>
</div>
<div className="flex gap-3">
<Button onClick={handleValidar} variant="primary" disabled={!seleccion}>
<CheckCircle className="w-4 h-4 mr-2" />
Validar Selección
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Cambiar Producción
</Button>
</div>
{validado && seleccion === plantaOptima?.planta && (
<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! La planta {plantaOptima.planta} tiene el menor CMe (${plantaOptima.cme.toFixed(2)})
</span>
</div>
</div>
)}
{validado && seleccion !== plantaOptima?.planta && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="text-error font-medium">
La planta {seleccion} no es la óptima. La planta {plantaOptima?.planta} tiene un CMe menor (${plantaOptima?.cme.toFixed(2)} vs ${calculos.find(c => c.planta === seleccion)?.cme.toFixed(2)}).
</p>
</div>
)}
</Card>
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Causas de las Economías de Escala:</h4>
<ul className="space-y-1 text-sm text-green-800">
<li> <strong>Especialización del trabajo:</strong> Tareas más específicas = mayor eficiencia</li>
<li> <strong>Tecnología especializada:</strong> Maquinaria más eficiente a gran escala</li>
<li> <strong>Descuentos por volumen:</strong> Comprar insumos al por mayor es más barato</li>
<li> <strong>División de costos fijos:</strong> Se reparten entre más unidades</li>
</ul>
</Card>
</div>
);
}
export default EconomiasEscala;

View File

@@ -0,0 +1,231 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Layers } from 'lucide-react';
interface Etapa {
id: string;
nombre: string;
descripcion: string;
color: string;
rango: string;
}
const ETAPAS: Etapa[] = [
{
id: 'i',
nombre: 'Etapa I',
descripcion: 'PMg creciente - Rendimientos crecientes a escala',
color: '#22c55e',
rango: '0 a 3 trabajadores'
},
{
id: 'ii',
nombre: 'Etapa II',
descripcion: 'PMg decreciente pero positivo - Rendimientos decrecientes',
color: '#3b82f6',
rango: '3 a 6 trabajadores'
},
{
id: 'iii',
nombre: 'Etapa III',
descripcion: 'PMg negativo - Producción total disminuye',
color: '#ef4444',
rango: 'Más de 6 trabajadores'
}
];
export function EtapasProduccion() {
const [respuestas, setRespuestas] = useState<{[key: number]: string}>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const preguntas = [
{
id: 1,
texto: '¿En qué etapa un productor racional NUNCA producirá?',
respuestaCorrecta: 'iii',
explicacion: 'En la Etapa III el producto marginal es negativo, lo que significa que agregar más trabajadores disminuye la producción total. Un productor racional evitará esta etapa.'
},
{
id: 2,
texto: '¿En qué etapa los rendimientos marginales son crecientes?',
respuestaCorrecta: 'i',
explicacion: 'En la Etapa I, cada trabajador adicional aporta más que el anterior debido a la especialización y división del trabajo.'
},
{
id: 3,
texto: '¿En qué etapa se encuentra la mayoría de la producción eficiente?',
respuestaCorrecta: 'ii',
explicacion: 'La Etapa II es donde opera un productor racional. Aunque los rendimientos marginales decrecen, siguen siendo positivos hasta cierto punto.'
}
];
const seleccionarRespuesta = (preguntaId: number, etapaId: string) => {
setRespuestas(prev => ({ ...prev, [preguntaId]: etapaId }));
setMostrarResultados(false);
};
const validarTodas = () => {
setMostrarResultados(true);
};
const todasRespondidas = preguntas.every(p => respuestas[p.id]);
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Etapas de la Producción"
subtitle="Identifica las tres etapas según la Ley de Rendimientos Decrecientes"
/>
<div className="space-y-6">
{/* Gráfico de etapas */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4">Producto Total y sus Etapas</h4>
<svg className="w-full h-72" viewBox="0 0 600 280">
{/* Ejes */}
<line x1="60" y1="240" x2="550" y2="240" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="240" x2="60" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas eje X */}
<text x="150" y="260" textAnchor="middle" className="text-xs fill-gray-600">L1</text>
<text x="300" y="260" textAnchor="middle" className="text-xs fill-gray-600">L2</text>
<text x="450" y="260" textAnchor="middle" className="text-xs fill-gray-600">L3</text>
<text x="300" y="275" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad de Trabajo (L)</text>
{/* Etiquetas eje Y */}
<text x="45" y="245" textAnchor="end" className="text-xs fill-gray-600">0</text>
<text x="45" y="180" textAnchor="end" className="text-xs fill-gray-600">Q1</text>
<text x="45" y="100" textAnchor="end" className="text-xs fill-gray-600">Q2</text>
<text x="45" y="40" textAnchor="end" className="text-xs fill-gray-600">Q3</text>
<text x="20" y="130" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 20 130)">Producto Total (PT)</text>
{/* Líneas verticales separadoras de etapas */}
<line x1="150" y1="20" x2="150" y2="240" stroke="#9ca3af" strokeWidth="2" strokeDasharray="8" />
<line x1="300" y1="20" x2="300" y2="240" stroke="#9ca3af" strokeWidth="2" strokeDasharray="8" />
{/* Zonas de etapas */}
<rect x="60" y="20" width="90" height="220" fill="#dcfce7" opacity="0.5" />
<rect x="150" y="20" width="150" height="220" fill="#dbeafe" opacity="0.5" />
<rect x="300" y="20" width="250" height="220" fill="#fee2e2" opacity="0.5" />
{/* Etiquetas de etapas */}
<text x="105" y="35" textAnchor="middle" className="text-sm font-bold fill-green-700">ETAPA I</text>
<text x="105" y="50" textAnchor="middle" className="text-xs fill-green-600">PMg creciente</text>
<text x="225" y="35" textAnchor="middle" className="text-sm font-bold fill-blue-700">ETAPA II</text>
<text x="225" y="50" textAnchor="middle" className="text-xs fill-blue-600">PMg decreciente</text>
<text x="425" y="35" textAnchor="middle" className="text-sm font-bold fill-red-700">ETAPA III</text>
<text x="425" y="50" textAnchor="middle" className="text-xs fill-red-600">PMg negativo</text>
{/* Curva de producto total */}
<path
d="M 60,240 Q 105,200 150,180 Q 200,140 300,100 Q 380,60 450,80 Q 500,120 550,200"
fill="none"
stroke="#1f2937"
strokeWidth="3"
/>
{/* Punto de inflexión */}
<circle cx="150" cy="180" r="6" fill="#22c55e" stroke="white" strokeWidth="2" />
<text x="150" y="170" textAnchor="middle" className="text-xs fill-green-700 font-bold">Punto de Inflexión</text>
{/* Punto máximo */}
<circle cx="450" cy="80" r="6" fill="#ef4444" stroke="white" strokeWidth="2" />
<text x="450" y="70" textAnchor="middle" className="text-xs fill-red-700 font-bold">PT Máximo</text>
{/* Flecha mostrando declive */}
<path d="M 480,100 L 520,160" stroke="#ef4444" strokeWidth="3" markerEnd="url(#arrowRed)" />
<defs>
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#ef4444" />
</marker>
</defs>
</svg>
</div>
{/* Leyenda de etapas */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{ETAPAS.map(etapa => (
<div key={etapa.id} className="p-3 rounded-lg border" style={{ backgroundColor: `${etapa.color}15`, borderColor: etapa.color }}>
<h5 className="font-semibold" style={{ color: etapa.color }}>{etapa.nombre}</h5>
<p className="text-xs text-gray-600 mt-1">{etapa.descripcion}</p>
<p className="text-xs text-gray-500 mt-1">{etapa.rango}</p>
</div>
))}
</div>
{/* Preguntas */}
<div className="space-y-4">
{preguntas.map(pregunta => (
<div key={pregunta.id} className="bg-white border rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-3">{pregunta.id}. {pregunta.texto}</h4>
<div className="flex gap-3">
{ETAPAS.map(etapa => {
const esCorrecta = mostrarResultados && respuestas[pregunta.id] === pregunta.respuestaCorrecta;
const esIncorrecta = mostrarResultados && respuestas[pregunta.id] === etapa.id && respuestas[pregunta.id] !== pregunta.respuestaCorrecta;
const esLaCorrecta = mostrarResultados && etapa.id === pregunta.respuestaCorrecta;
return (
<button
key={etapa.id}
onClick={() => seleccionarRespuesta(pregunta.id, etapa.id)}
disabled={mostrarResultados}
className={`flex-1 p-3 rounded-lg border-2 text-center transition-all ${
respuestas[pregunta.id] === etapa.id && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: esCorrecta
? 'border-green-500 bg-green-50'
: esIncorrecta
? 'border-red-500 bg-red-50'
: esLaCorrecta
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-bold" style={{ color: etapa.color }}>{etapa.nombre}</span>
{mostrarResultados && esLaCorrecta && (
<CheckCircle className="w-4 h-4 text-green-600 mx-auto mt-1" />
)}
{mostrarResultados && esIncorrecta && (
<XCircle className="w-4 h-4 text-red-600 mx-auto mt-1" />
)}
</button>
);
})}
</div>
{mostrarResultados && (
<div className={`mt-3 p-3 rounded text-sm ${respuestas[pregunta.id] === pregunta.respuestaCorrecta ? 'bg-green-50 text-green-800' : 'bg-amber-50 text-amber-800'}`}>
{pregunta.explicacion}
</div>
)}
</div>
))}
</div>
<Button onClick={validarTodas} disabled={!todasRespondidas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Layers className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-900">Conclusión</span>
</div>
<p className="text-sm text-blue-800">
Un productor racional opera principalmente en la <strong>Etapa II</strong>,
donde aunque los rendimientos marginales decrecen, siguen siendo positivos.
La Etapa I es muy corta y la Etapa III es irracional desde el punto de vista económico.
</p>
</div>
)}
</div>
</Card>
</div>
);
}
export default EtapasProduccion;

View File

@@ -0,0 +1,184 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Factory, Calculator } from 'lucide-react';
interface FuncionProduccionProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function FuncionProduccion({ ejercicioId: _ejercicioId, onComplete }: FuncionProduccionProps) {
const [capital, setCapital] = useState(4);
const [trabajo, setTrabajo] = useState(5);
const tablaProduccion = [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 8, 12, 17, 20, 22, 23, 23],
[0, 12, 20, 28, 35, 40, 43, 44],
[0, 17, 28, 40, 50, 58, 63, 65],
[0, 20, 35, 50, 65, 75, 83, 87],
[0, 22, 40, 58, 75, 88, 98, 104],
[0, 23, 43, 63, 83, 98, 110, 118],
[0, 23, 44, 65, 87, 104, 118, 128],
];
const output = useMemo(() => {
if (trabajo >= 0 && trabajo <= 7 && capital >= 0 && capital <= 7) {
return tablaProduccion[capital][trabajo];
}
return 0;
}, [capital, trabajo]);
const handleCompletar = () => {
if (onComplete) {
onComplete(100);
}
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Función de Producción: Q = f(K, L)"
subtitle="Observa cómo cambia la producción al variar los factores productivos"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Factory className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Concepto</span>
</div>
<p className="text-sm text-blue-700">
La función de producción muestra la relación técnica entre los factores productivos
(Capital K y Trabajo L) y la cantidad máxima de output (Q) que puede producirse.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700">
Capital (K) - Unidades de maquinaria
</label>
<div className="flex items-center gap-4">
<Input
type="range"
min="1"
max="7"
value={capital}
onChange={(e) => setCapital(parseInt(e.target.value))}
className="flex-1"
/>
<span className="font-mono text-lg font-bold text-primary w-12">
{capital}
</span>
</div>
</div>
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700">
Trabajo (L) - Número de trabajadores
</label>
<div className="flex items-center gap-4">
<Input
type="range"
min="1"
max="7"
value={trabajo}
onChange={(e) => setTrabajo(parseInt(e.target.value))}
className="flex-1"
/>
<span className="font-mono text-lg font-bold text-primary w-12">
{trabajo}
</span>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
<div className="flex items-center justify-center gap-4">
<Calculator className="w-8 h-8 text-green-600" />
<div className="text-center">
<p className="text-sm text-green-700 mb-1">Output Total (Q)</p>
<p className="text-4xl font-bold text-green-800">
Q = f({capital}, {trabajo}) = {output}
</p>
<p className="text-sm text-green-600 mt-2">
unidades producidas
</p>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-3 py-2 border text-left">K \ L</th>
{[0, 1, 2, 3, 4, 5, 6, 7].map(l => (
<th key={l} className="px-3 py-2 border text-center">{l}</th>
))}
</tr>
</thead>
<tbody>
{tablaProduccion.map((fila, k) => (
<tr key={k} className={k === capital ? 'bg-blue-50' : ''}>
<td className="px-3 py-2 border font-medium bg-gray-50">{k}</td>
{fila.map((q, l) => (
<td
key={l}
className={`px-3 py-2 border text-center ${
k === capital && l === trabajo
? 'bg-green-200 font-bold text-green-800'
: ''
}`}
>
{q}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 text-sm text-gray-600">
<p><strong>Nota:</strong> La celda resaltada en verde muestra el output actual.
Las filas representan niveles de Capital (K) y las columnas niveles de Trabajo (L).</p>
</div>
</Card>
<Card className="bg-gradient-to-r from-purple-50 to-blue-50">
<h4 className="font-semibold text-gray-900 mb-3">Ejercicio de Comprensión</h4>
<div className="space-y-4">
<p className="text-sm text-gray-700">
Si una empresa tiene <strong>3 unidades de capital</strong> y contrata <strong>4 trabajadores</strong>,
¿cuál es el nivel de producción máximo alcanzable según la tabla?
</p>
<div className="flex items-center gap-4">
<Input
type="number"
placeholder="Ingresa el valor de Q"
className="w-40"
readOnly
value={tablaProduccion[3][4]}
/>
<span className="text-sm text-gray-600">
Respuesta correcta: {tablaProduccion[3][4]} unidades
</span>
</div>
</div>
</Card>
<div className="flex justify-end">
<Button onClick={handleCompletar} size="lg">
<CheckCircle className="w-5 h-5 mr-2" />
Marcar como Completado
</Button>
</div>
</div>
);
}
export default FuncionProduccion;

View File

@@ -0,0 +1,278 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Scale, RotateCcw, TrendingUp } from 'lucide-react';
interface IngresoCompetenciaPerfectaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function IngresoCompetenciaPerfecta({ ejercicioId: _ejercicioId, onComplete }: IngresoCompetenciaPerfectaProps) {
const PRECIO_MERCADO = 50;
const [cantidad, setCantidad] = useState(100);
const [respuestas, setRespuestas] = useState({
it: '',
img: '',
relacion: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const ingresoTotal = useMemo(() => PRECIO_MERCADO * cantidad, [cantidad]);
const ingresoMarginal = PRECIO_MERCADO;
const ingresoPromedio = PRECIO_MERCADO;
const datosTabla = useMemo(() => {
const datos = [];
for (let q = 0; q <= 200; q += 25) {
datos.push({
q,
p: PRECIO_MERCADO,
it: PRECIO_MERCADO * q,
img: PRECIO_MERCADO,
ip: PRECIO_MERCADO,
});
}
return datos;
}, []);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseFloat(respuestas.it) !== ingresoTotal) {
nuevosErrores.push(`IT incorrecto. IT = P × Q = ${PRECIO_MERCADO} × ${cantidad}`);
}
if (parseFloat(respuestas.img) !== ingresoMarginal) {
nuevosErrores.push(`IMg incorrecto. En competencia perfecta, IMg = P`);
}
if (!['igual', 'igual a', 'es igual', 'son iguales'].some(r => respuestas.relacion.toLowerCase().includes(r))) {
nuevosErrores.push('En competencia perfecta, P = IMg = IPMe');
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setCantidad(100);
setRespuestas({ it: '', img: '', relacion: '' });
setValidado(false);
setErrores([]);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso en Competencia Perfecta"
subtitle="Precio = Ingreso Marginal = Ingreso Promedio"
/>
<div className="bg-green-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Scale className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-800">Características</span>
</div>
<p className="text-sm text-green-700">
En competencia perfecta, la empresa es tomadora de precios. El precio de mercado
es constante e independiente de la cantidad que produzca la empresa. Por eso:
<strong>P = IMg = IPMe</strong>. La curva de demanda es horizontal (perfectamente elástica).
</p>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="bg-blue-100 p-4 rounded-lg text-center border-2 border-blue-300">
<p className="text-sm text-blue-700 mb-1 font-medium">Precio de Mercado (P)</p>
<p className="text-3xl font-bold text-blue-800">${PRECIO_MERCADO}</p>
<p className="text-xs text-blue-600 mt-1">Constante</p>
</div>
<div className="bg-purple-100 p-4 rounded-lg text-center border-2 border-purple-300">
<p className="text-sm text-purple-700 mb-1 font-medium">Ingreso Marginal (IMg)</p>
<p className="text-3xl font-bold text-purple-800">${ingresoMarginal}</p>
<p className="text-xs text-purple-600 mt-1">=P</p>
</div>
<div className="bg-orange-100 p-4 rounded-lg text-center border-2 border-orange-300">
<p className="text-sm text-orange-700 mb-1 font-medium">Ingreso Promedio (IPMe)</p>
<p className="text-3xl font-bold text-orange-800">${ingresoPromedio}</p>
<p className="text-xs text-orange-600 mt-1">=P</p>
</div>
</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): {cantidad} unidades
</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="200"
step="10"
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="bg-green-50 border-2 border-green-200 p-6 rounded-lg mb-6">
<div className="text-center">
<p className="text-sm text-green-700 mb-2">Ingreso Total con Q = {cantidad}</p>
<p className="text-4xl font-bold text-green-800">
IT = ${ingresoTotal.toLocaleString()}
</p>
<p className="text-sm text-green-600 mt-2">
{PRECIO_MERCADO} × {cantidad} = ${ingresoTotal.toLocaleString()}
</p>
</div>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IMg ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IPMe ($)</th>
</tr>
</thead>
<tbody>
{datosTabla.filter((_, i) => i % 2 === 0 || i === datosTabla.length - 1).map((d, i) => (
<tr
key={i}
className={`border-b hover:bg-gray-50 ${d.q === cantidad ? 'bg-green-50' : ''}`}
>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2 text-blue-600 font-medium">${d.p}</td>
<td className="px-3 py-2">${d.it.toLocaleString()}</td>
<td className="px-3 py-2 text-purple-600">${d.img}</td>
<td className="px-3 py-2 text-orange-600">${d.ip}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-blue-50 to-green-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-blue-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">
¿Cuál es el IT? ($)
</label>
<Input
type="number"
value={respuestas.it}
onChange={(e) => handleRespuestaChange('it', e.target.value)}
className="w-full"
placeholder={String(PRECIO_MERCADO * cantidad)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cuál es el IMg? ($)
</label>
<Input
type="number"
value={respuestas.img}
onChange={(e) => handleRespuestaChange('img', e.target.value)}
className="w-full"
placeholder="?"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cómo se relacionan P, IMg e IPMe?
</label>
<Input
type="text"
value={respuestas.relacion}
onChange={(e) => handleRespuestaChange('relacion', e.target.value)}
className="w-full"
placeholder="Son iguales / Diferentes"
/>
</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! En competencia perfecta: P = IMg = IPMe = ${PRECIO_MERCADO}</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-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Resumen - Competencia Perfecta:</h4>
<div className="grid md:grid-cols-2 gap-4 text-sm text-green-800">
<div>
<p className="font-medium mb-1">Fórmulas:</p>
<ul className="space-y-1">
<li> IT = P × Q</li>
<li> IMg = P (constante)</li>
<li> IPMe = P (constante)</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">Características:</p>
<ul className="space-y-1">
<li> La empresa es tomadora de precios</li>
<li> Demanda horizontal (perfectamente elástica)</li>
<li> P = IMg = IPMe</li>
</ul>
</div>
</div>
</Card>
</div>
);
}
export default IngresoCompetenciaPerfecta;

View File

@@ -0,0 +1,234 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Activity, RotateCcw, Calculator } from 'lucide-react';
interface IngresoMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaIngreso {
q: number;
p: number;
}
export function IngresoMarginal({ ejercicioId: _ejercicioId, onComplete }: IngresoMarginalProps) {
const datosBase: FilaIngreso[] = [
{ q: 0, p: 100 },
{ q: 1, p: 90 },
{ q: 2, p: 80 },
{ q: 3, p: 70 },
{ q: 4, p: 60 },
{ q: 5, p: 50 },
{ q: 6, p: 40 },
{ q: 7, p: 30 },
{ q: 8, p: 20 },
];
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados = useMemo(() => {
return datosBase.map((fila, index) => {
const it = fila.p * fila.q;
const itAnterior = index > 0 ? datosBase[index - 1].p * datosBase[index - 1].q : 0;
const img = index > 0 ? it - itAnterior : null;
return { ...fila, it, img };
});
}, []);
const handleRespuestaChange = (q: number, valor: string) => {
setRespuestas(prev => ({ ...prev, [`img_${q}`]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
datosCalculados.forEach((fila) => {
if (fila.img !== null) {
const respuesta = parseFloat(respuestas[`img_${fila.q}`] || '0');
if (Math.abs(respuesta - fila.img) > 1) {
nuevosErrores.push(`Q=${fila.q}: El IMg debería ser $${fila.img}`);
}
}
});
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({});
setValidado(false);
setErrores([]);
};
const maxIT = Math.max(...datosCalculados.map(d => d.it));
const maxIMG = Math.max(...datosCalculados.filter(d => d.img !== null).map(d => Math.abs(d.img || 0)));
const escalaIT = maxIT > 0 ? 120 / maxIT : 1;
const escalaIMG = maxIMG > 0 ? 60 / maxIMG : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso Marginal (IMg)"
subtitle="El ingreso adicional por vender una unidad más"
/>
<div className="bg-purple-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Activity className="w-5 h-5 text-purple-600" />
<span className="font-semibold text-purple-800">Concepto</span>
</div>
<p className="text-sm text-purple-700">
El Ingreso Marginal es el cambio en el ingreso total resultante de vender
una unidad adicional. Se calcula como: <strong>IMg = ΔIT / ΔQ</strong>.
Cuando el precio debe bajar para vender más, el IMg {'<'} IT.
</p>
</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)">$ (×100)</text>
{datosBase.map((d, i) => (
<g key={i}>
<line x1={60 + i * 35} y1="160" x2={60 + i * 35} y2="165" stroke="#374151" strokeWidth="1" />
<text x={60 + i * 35} y="175" textAnchor="middle" className="text-xs fill-gray-500">{d.q}</text>
</g>
))}
<polyline
fill="none"
stroke="#10b981"
strokeWidth="2"
points={datosCalculados.map((d, i) => `${60 + i * 35},${160 - d.it * escalaIT}`).join(' ')}
/>
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="2"
strokeDasharray="4"
points={datosCalculados
.filter(d => d.img !== null)
.map((d, i) => `${95 + i * 35},${160 - (d.img || 0) * escalaIMG - 50}`)
.join(' ')}
/>
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#10b981" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">IT</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#7c3aed" strokeWidth="2" strokeDasharray="4" />
<text x="25" y="19" className="text-xs fill-gray-600">IMg</text>
</g>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">IMg ($)</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila) => (
<tr key={fila.q} className="border-b hover:bg-gray-50">
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2">{fila.p}</td>
<td className="px-3 py-2 font-medium text-green-600">{fila.it}</td>
<td className="px-3 py-2 bg-blue-50">
{fila.img !== null ? (
<Input
type="number"
value={respuestas[`img_${fila.q}`] || ''}
onChange={(e) => handleRespuestaChange(fila.q, e.target.value)}
className="w-24"
placeholder="IMg"
/>
) : (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg mb-4">
<h4 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
<Calculator className="w-5 h-5 text-purple-600" />
Cálculo del Ingreso Marginal:
</h4>
<p className="text-sm text-gray-700 mb-2">
IMg = IT(Q) - IT(Q-1)
</p>
<p className="text-sm text-gray-600">
Ejemplo: Cuando Q aumenta de 2 a 3 unidades, el IT pasa de $160 a $210.
El IMg de la 3ra unidad es $210 - $160 = $50.
</p>
</div>
<div className="flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Cálculos
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Limpiar
</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">¡Todos los cálculos son correctos!</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">Errores encontrados:</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-purple-50 border-purple-200">
<h4 className="font-semibold text-purple-900 mb-2">Importancia del Ingreso Marginal:</h4>
<ul className="space-y-1 text-sm text-purple-800">
<li> <strong>Regla de maximización:</strong> La empresa maximiza beneficios cuando IMg = CMg</li>
<li> <strong>IMg {'<'} P:</strong> Cuando debe bajar el precio para vender más, el IMg es menor que el precio</li>
<li> <strong>IMg positivo:</strong> Mientras IMg {'>'} 0, el ingreso total aumenta</li>
<li> <strong>IMg negativo:</strong> Si IMg {'<'} 0, vender más reduce el ingreso total</li>
</ul>
</Card>
</div>
);
}
export default IngresoMarginal;

View File

@@ -0,0 +1,273 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, DollarSign, RotateCcw, TrendingUp } from 'lucide-react';
interface IngresoTotalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Producto {
nombre: string;
precio: number;
}
export function IngresoTotal({ ejercicioId: _ejercicioId, onComplete }: IngresoTotalProps) {
const productos: Producto[] = [
{ nombre: 'Libros', precio: 25 },
{ nombre: 'Electrónicos', precio: 150 },
{ nombre: 'Ropa', precio: 45 },
];
const [productoSeleccionado, setProductoSeleccionado] = useState(0);
const [cantidad, setCantidad] = useState(100);
const [respuestaIT, setRespuestaIT] = useState('');
const [validado, setValidado] = useState(false);
const [error, setError] = useState('');
const precio = productos[productoSeleccionado].precio;
const ingresoTotal = useMemo(() => precio * cantidad, [precio, cantidad]);
const datosTabla = useMemo(() => {
const datos = [];
for (let q = 0; q <= 200; q += 20) {
datos.push({ q, it: precio * q });
}
return datos;
}, [precio]);
const handleValidar = () => {
const respuesta = parseFloat(respuestaIT);
if (Math.abs(respuesta - ingresoTotal) < 1) {
setError('');
setValidado(true);
if (onComplete) {
onComplete(100);
}
} else {
setError(`Incorrecto. IT = P × Q = $${precio} × ${cantidad} = $${ingresoTotal.toLocaleString()}`);
setValidado(true);
}
};
const reiniciar = () => {
setCantidad(100);
setRespuestaIT('');
setValidado(false);
setError('');
};
const maxIT = Math.max(...datosTabla.map(d => d.it));
const escalaY = maxIT > 0 ? 120 / maxIT : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso Total (IT)"
subtitle="IT = Precio × Cantidad vendida"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<DollarSign className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Fórmula Fundamental</span>
</div>
<p className="text-sm text-blue-700">
El Ingreso Total representa el dinero total que recibe una empresa por la venta
de sus productos. Se calcula multiplicando el precio de venta por la cantidad
vendida: <strong>IT = P × Q</strong>
</p>
</div>
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Producto
</label>
<select
value={productoSeleccionado}
onChange={(e) => {
setProductoSeleccionado(parseInt(e.target.value));
setValidado(false);
setRespuestaIT('');
}}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
>
{productos.map((p, i) => (
<option key={i} value={i}>
{p.nombre} - ${p.precio} c/u
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Cantidad vendida (Q): {cantidad} unidades
</label>
<input
type="range"
min="0"
max="200"
step="10"
value={cantidad}
onChange={(e) => {
setCantidad(parseInt(e.target.value));
setValidado(false);
setRespuestaIT('');
}}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="bg-gray-50 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 mb-1">Precio (P)</p>
<p className="text-2xl font-bold text-primary">${precio}</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 mb-1">Cantidad (Q)</p>
<p className="text-2xl font-bold text-secondary">{cantidad}</p>
</div>
<div className="bg-green-50 p-4 rounded-lg text-center border-2 border-green-200">
<p className="text-sm text-green-600 mb-1">Ingreso Total (IT)</p>
<p className="text-3xl font-bold text-green-700">${ingresoTotal.toLocaleString()}</p>
</div>
</div>
<div className="h-48 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 150">
<line x1="40" y1="130" x2="380" y2="130" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="130" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="145" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="75" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 75)">IT ($)</text>
{[0, 50, 100, 150, 200].map((q) => (
<g key={q}>
<line x1={40 + (q / 200) * 300} y1="130" x2={40 + (q / 200) * 300} y2="135" stroke="#374151" strokeWidth="1" />
<text x={40 + (q / 200) * 300} y="145" textAnchor="middle" className="text-xs fill-gray-500">{q}</text>
</g>
))}
<line
x1="40"
y1="130"
x2="340"
y2={130 - (datosTabla[datosTabla.length - 1].it * escalaY)}
stroke="#10b981"
strokeWidth="3"
/>
<circle
cx={40 + (cantidad / 200) * 300}
cy={130 - (ingresoTotal * escalaY)}
r="6"
fill="#10b981"
stroke="white"
strokeWidth="2"
/>
<text
x={40 + (cantidad / 200) * 300}
y={120 - (ingresoTotal * escalaY)}
textAnchor="middle"
className="text-xs fill-green-700 font-bold"
>
(${ingresoTotal.toLocaleString()})
</text>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
</tr>
</thead>
<tbody>
{datosTabla.filter((_, i) => i % 2 === 0).map((d, i) => (
<tr
key={i}
className={`border-b hover:bg-gray-50 ${d.q === cantidad ? 'bg-green-50' : ''}`}
>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2">${precio}</td>
<td className="px-3 py-2 font-medium text-primary">${d.it.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
Calcula el Ingreso Total:
</h4>
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
IT = P × Q = ${precio} × {cantidad} = ?
</label>
<Input
type="number"
value={respuestaIT}
onChange={(e) => {
setRespuestaIT(e.target.value);
setValidado(false);
}}
className="w-full"
placeholder="Ingresa el IT"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={handleValidar} variant="primary" disabled={!respuestaIT}>
<CheckCircle className="w-4 h-4 mr-2" />
Validar
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Cambiar valores
</Button>
</div>
{validado && !error && (
<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! IT = ${ingresoTotal.toLocaleString()}</span>
</div>
</div>
)}
{validado && error && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="text-error">{error}</p>
</div>
)}
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmula del Ingreso Total:</h4>
<div className="text-center py-4">
<p className="text-2xl font-bold text-blue-800">IT = P × Q</p>
<p className="text-sm text-blue-600 mt-2">
Donde: IT = Ingreso Total, P = Precio, Q = Cantidad vendida
</p>
</div>
</Card>
</div>
);
}
export default IngresoTotal;

View File

@@ -0,0 +1,173 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, TrendingDown } from 'lucide-react';
export function LeyRendimientosDecrecientes() {
const [respuesta, setRespuesta] = useState<string | null>(null);
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const validarRespuesta = () => {
setMostrarExplicacion(true);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ley de Rendimientos Decrecientes"
subtitle="Comprende cómo los rendimientos marginales disminuyen a medida que aumenta una variable productiva"
/>
<div className="space-y-4">
<div className="bg-amber-50 p-4 rounded-lg border border-amber-200">
<p className="text-sm text-amber-800">
<strong>Escenario:</strong> Un granjero tiene 100 hectáreas de tierra fijas.
Puede contratar más trabajadores, pero la cantidad de tierra no cambia.
</p>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4">Producción de Trigo (toneladas)</h4>
<svg className="w-full h-64" viewBox="0 0 500 220">
{/* Ejes */}
<line x1="50" y1="180" x2="450" y2="180" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="180" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas eje X - Trabajadores */}
<text x="90" y="200" textAnchor="middle" className="text-xs fill-gray-600">1</text>
<text x="170" y="200" textAnchor="middle" className="text-xs fill-gray-600">2</text>
<text x="250" y="200" textAnchor="middle" className="text-xs fill-gray-600">3</text>
<text x="330" y="200" textAnchor="middle" className="text-xs fill-gray-600">4</text>
<text x="410" y="200" textAnchor="middle" className="text-xs fill-gray-600">5</text>
<text x="250" y="215" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Número de Trabajadores</text>
{/* Etiquetas eje Y - Producción */}
<text x="35" y="185" textAnchor="end" className="text-xs fill-gray-600">0</text>
<text x="35" y="145" textAnchor="end" className="text-xs fill-gray-600">50</text>
<text x="35" y="105" textAnchor="end" className="text-xs fill-gray-600">100</text>
<text x="35" y="65" textAnchor="end" className="text-xs fill-gray-600">150</text>
<text x="35" y="25" textAnchor="end" className="text-xs fill-gray-600">200</text>
<text x="15" y="100" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 15 100)">Producción (Tn)</text>
{/* Líneas de cuadrícula */}
<line x1="50" y1="140" x2="450" y2="140" stroke="#e5e7eb" strokeWidth="1" strokeDasharray="4" />
<line x1="50" y1="100" x2="450" y2="100" stroke="#e5e7eb" strokeWidth="1" strokeDasharray="4" />
<line x1="50" y1="60" x2="450" y2="60" stroke="#e5e7eb" strokeWidth="1" strokeDasharray="4" />
{/* Curva de producción total */}
<path
d="M 50,180 Q 90,140 170,100 Q 250,70 330,60 Q 410,55 450,65"
fill="none"
stroke="#2563eb"
strokeWidth="3"
/>
{/* Puntos de datos */}
<circle cx="90" cy="140" r="6" fill="#2563eb" />
<circle cx="170" cy="100" r="6" fill="#2563eb" />
<circle cx="250" cy="75" r="6" fill="#2563eb" />
<circle cx="330" cy="65" r="6" fill="#2563eb" />
<circle cx="410" cy="68" r="6" fill="#ef4444" />
{/* Etiquetas de puntos */}
<text x="90" y="125" textAnchor="middle" className="text-xs fill-gray-700">50Tn</text>
<text x="170" y="85" textAnchor="middle" className="text-xs fill-gray-700">100Tn</text>
<text x="250" y="60" textAnchor="middle" className="text-xs fill-gray-700">135Tn</text>
<text x="330" y="50" textAnchor="middle" className="text-xs fill-gray-700">155Tn</text>
<text x="410" y="53" textAnchor="middle" className="text-xs fill-red-600 font-bold">160Tn</text>
{/* Flecha indicando decrecimiento */}
<path d="M 370,50 Q 390,45 400,60" fill="none" stroke="#ef4444" strokeWidth="2" markerEnd="url(#arrow)" />
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#ef4444" />
</marker>
</defs>
</svg>
</div>
<div className="bg-white border rounded-lg p-4">
<h4 className="font-semibold text-gray-900 mb-3">
¿Qué observas en el punto del 5to trabajador?
</h4>
<div className="space-y-2">
<button
onClick={() => setRespuesta('a')}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === 'a'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-medium">a)</span> La producción aumenta más rápido que antes
</button>
<button
onClick={() => setRespuesta('b')}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === 'b'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-medium">b)</span> El incremento de producción es menor (solo 5Tn adicionales)
</button>
<button
onClick={() => setRespuesta('c')}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === 'c'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-medium">c)</span> La producción total disminuye
</button>
</div>
</div>
<Button onClick={validarRespuesta} disabled={!respuesta}>
Validar Respuesta
</Button>
{mostrarExplicacion && (
<div className={`p-4 rounded-lg border ${respuesta === 'b' ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-center gap-2 mb-2">
{respuesta === 'b' ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<XCircle className="w-5 h-5 text-red-600" />
)}
<span className={`font-semibold ${respuesta === 'b' ? 'text-green-800' : 'text-red-800'}`}>
{respuesta === 'b' ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className={`text-sm ${respuesta === 'b' ? 'text-green-700' : 'text-red-700'}`}>
La respuesta correcta es <strong>b)</strong>. Con el 5to trabajador, la producción
solo aumenta de 155Tn a 160Tn (5Tn adicionales), mientras que el 2do trabajador
aportó 50Tn adicionales. Esto demuestra la <strong>Ley de Rendimientos Decrecientes</strong>:
a medida que aumentamos una variable productiva (trabajo) manteniendo fijas las demás
(tierra), el producto marginal disminuye.
</p>
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
<TrendingDown className="w-5 h-5" />
Fórmula del Producto Marginal
</h4>
<p className="text-sm text-blue-800">
<strong>PMg = ΔProducción Total / ΔTrabajadores</strong>
</p>
<p className="text-sm text-blue-700 mt-2">
PMg (12) = (100-50)/(2-1) = 50 Tn<br />
PMg (45) = (160-155)/(5-4) = 5 Tn
</p>
</Card>
</div>
);
}
export default LeyRendimientosDecrecientes;

View File

@@ -0,0 +1,233 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Calculator, TrendingDown, TrendingUp } from 'lucide-react';
interface ProductoMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDatos {
L: number;
PT: number;
PMg: number | null;
}
export function ProductoMarginal({ ejercicioId: _ejercicioId, onComplete }: ProductoMarginalProps) {
const [respuestas, setRespuestas] = useState<Record<number, string>>({});
const [verificado, setVerificado] = useState(false);
const datosBase = [
{ L: 0, PT: 0 },
{ L: 1, PT: 10 },
{ L: 2, PT: 25 },
{ L: 3, PT: 45 },
{ L: 4, PT: 60 },
{ L: 5, PT: 70 },
{ L: 6, PT: 75 },
{ L: 7, PT: 75 },
{ L: 8, PT: 70 },
];
const datosCompletos: FilaDatos[] = useMemo(() => {
return datosBase.map((fila, index) => ({
L: fila.L,
PT: fila.PT,
PMg: index > 0 ? fila.PT - datosBase[index - 1].PT : null,
}));
}, []);
const handleInputChange = (L: number, value: string) => {
setRespuestas(prev => ({ ...prev, [L]: value }));
};
const handleVerificar = () => {
setVerificado(true);
let correctas = 0;
let total = 0;
datosCompletos.forEach(fila => {
if (fila.PMg !== null) {
total++;
if (parseInt(respuestas[fila.L]) === fila.PMg) {
correctas++;
}
}
});
if (correctas === total && onComplete) {
onComplete(100);
}
};
const handleReiniciar = () => {
setRespuestas({});
setVerificado(false);
};
const todasRespondidas = datosCompletos
.filter(f => f.PMg !== null)
.every(f => respuestas[f.L] !== undefined && respuestas[f.L] !== '');
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Producto Marginal (PMg)"
subtitle="Calcula el cambio en el producto total al aumentar una unidad de trabajo"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Calculator className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Fórmula</span>
</div>
<div className="bg-white p-3 rounded border border-blue-200">
<p className="font-mono text-lg text-center text-blue-900">
PMg = ΔPT / ΔL = (PT - PT) / (L - L)
</p>
</div>
<p className="text-sm text-blue-700 mt-3">
El <strong>Producto Marginal</strong> mide la producción adicional generada
al emplear una unidad más de trabajo, manteniendo constante el capital.
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-3 border text-left">Trabajo (L)</th>
<th className="px-4 py-3 border text-left">Producto Total (PT)</th>
<th className="px-4 py-3 border text-left">Producto Marginal (PMg)</th>
<th className="px-4 py-3 border text-center">Estado</th>
</tr>
</thead>
<tbody>
{datosCompletos.map((fila) => (
<tr key={fila.L} className="border-b">
<td className="px-4 py-3 border font-medium">{fila.L}</td>
<td className="px-4 py-3 border font-mono">{fila.PT}</td>
<td className="px-4 py-3 border">
{fila.PMg === null ? (
<span className="text-gray-400"></span>
) : (
<div className="flex items-center gap-2">
<Input
type="number"
value={respuestas[fila.L] || ''}
onChange={(e) => handleInputChange(fila.L, e.target.value)}
disabled={verificado}
className={`w-24 ${
verificado
? parseInt(respuestas[fila.L]) === fila.PMg
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}`}
placeholder="?"
/>
{verificado && (
<span className={
parseInt(respuestas[fila.L]) === fila.PMg
? 'text-success text-sm'
: 'text-error text-sm'
}>
{parseInt(respuestas[fila.L]) === fila.PMg ? '✓' : `${fila.PMg}`}
</span>
)}
</div>
)}
</td>
<td className="px-4 py-3 border text-center">
{fila.PMg !== null && (
<>
{fila.PMg > (datosCompletos[fila.L - 1]?.PMg || 0) ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
<TrendingUp className="w-3 h-3 mr-1" />
Creciente
</span>
) : fila.PMg > 0 ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Decreciente
</span>
) : (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
<TrendingDown className="w-3 h-3 mr-1" />
Negativo
</span>
)}
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 bg-gray-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-3">Ley de los Rendimientos Marginales Decrecientes</h4>
<p className="text-sm text-gray-700 mb-3">
A medida que se agregan más unidades de un factor variable (trabajo) a un factor
fijo (capital), el producto marginal eventualmente disminuirá.
</p>
<div className="grid md:grid-cols-3 gap-3 text-sm">
<div className="bg-green-50 p-3 rounded border border-green-200">
<p className="font-medium text-green-800">Fase 1: PMg creciente</p>
<p className="text-green-700">Especialización y eficiencia</p>
</div>
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
<p className="font-medium text-yellow-800">Fase 2: PMg decreciente</p>
<p className="text-yellow-700">Ley de rendimientos decrecientes</p>
</div>
<div className="bg-red-50 p-3 rounded border border-red-200">
<p className="font-medium text-red-800">Fase 3: PMg negativo</p>
<p className="text-red-700">Hacinamiento/sobrepoblación</p>
</div>
</div>
</div>
</Card>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
{!verificado ? (
<span>Completa todos los campos para verificar</span>
) : (
<span>
Correctos: {datosCompletos.filter(f =>
f.PMg !== null && parseInt(respuestas[f.L]) === f.PMg
).length} / {datosCompletos.filter(f => f.PMg !== null).length}
</span>
)}
</div>
<div className="flex gap-3">
{!verificado ? (
<Button onClick={handleVerificar} disabled={!todasRespondidas}>
Verificar Cálculos
</Button>
) : (
<>
<Button onClick={handleReiniciar} variant="outline">
Reiniciar
</Button>
{datosCompletos.filter(f =>
f.PMg !== null && parseInt(respuestas[f.L]) === f.PMg
).length === datosCompletos.filter(f => f.PMg !== null).length && (
<Button onClick={() => onComplete?.(100)}>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</Button>
)}
</>
)}
</div>
</div>
</div>
);
}
export default ProductoMarginal;

View File

@@ -0,0 +1,247 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Divide, ArrowRight } from 'lucide-react';
interface ProductoMedioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDatos {
L: number;
PT: number;
PMe: number | null;
}
export function ProductoMedio({ ejercicioId: _ejercicioId, onComplete }: ProductoMedioProps) {
const [respuestas, setRespuestas] = useState<Record<number, string>>({});
const [verificado, setVerificado] = useState(false);
const datosBase = [
{ L: 1, PT: 10 },
{ L: 2, PT: 24 },
{ L: 3, PT: 39 },
{ L: 4, PT: 52 },
{ L: 5, PT: 60 },
{ L: 6, PT: 66 },
{ L: 7, PT: 70 },
{ L: 8, PT: 72 },
];
const datosCompletos: FilaDatos[] = useMemo(() => {
return datosBase.map(fila => ({
L: fila.L,
PT: fila.PT,
PMe: fila.L > 0 ? parseFloat((fila.PT / fila.L).toFixed(2)) : null,
}));
}, []);
const maxPMe = Math.max(...datosCompletos.map(d => d.PMe || 0));
const maxPMeL = datosCompletos.find(d => d.PMe === maxPMe)?.L;
const handleInputChange = (L: number, value: string) => {
setRespuestas(prev => ({ ...prev, [L]: value }));
};
const handleVerificar = () => {
setVerificado(true);
let correctas = 0;
datosCompletos.forEach(fila => {
const respuesta = parseFloat(respuestas[fila.L]);
if (Math.abs(respuesta - (fila.PMe || 0)) < 0.1) {
correctas++;
}
});
if (correctas === datosCompletos.length && onComplete) {
onComplete(100);
}
};
const handleReiniciar = () => {
setRespuestas({});
setVerificado(false);
};
const todasRespondidas = datosCompletos.every(f =>
respuestas[f.L] !== undefined && respuestas[f.L] !== ''
);
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Producto Medio (PMe)"
subtitle="Calcula el output por unidad de trabajo empleada"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Divide className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Fórmula</span>
</div>
<div className="bg-white p-3 rounded border border-blue-200">
<p className="font-mono text-lg text-center text-blue-900">
PMe = PT / L = Q / L
</p>
</div>
<p className="text-sm text-blue-700 mt-3">
El <strong>Producto Medio</strong> representa la producción por trabajador.
Mide la eficiencia promedio del factor trabajo.
</p>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-3 border text-left">Trabajo (L)</th>
<th className="px-4 py-3 border text-left">Producto Total (PT)</th>
<th className="px-4 py-3 border text-left">Producto Medio (PMe)</th>
<th className="px-4 py-3 border text-center">Estado</th>
</tr>
</thead>
<tbody>
{datosCompletos.map((fila) => (
<tr
key={fila.L}
className={`border-b ${fila.PMe === maxPMe ? 'bg-green-50' : ''}`}
>
<td className="px-4 py-3 border font-medium">{fila.L}</td>
<td className="px-4 py-3 border font-mono">{fila.PT}</td>
<td className="px-4 py-3 border">
<div className="flex items-center gap-2">
<Input
type="number"
step="0.01"
value={respuestas[fila.L] || ''}
onChange={(e) => handleInputChange(fila.L, e.target.value)}
disabled={verificado}
className={`w-24 ${
verificado
? Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}`}
placeholder="?"
/>
{verificado && (
<span className={
Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1
? 'text-success text-sm'
: 'text-error text-sm'
}>
{Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1
? '✓'
: `${fila.PMe}`}
</span>
)}
</div>
</td>
<td className="px-4 py-3 border text-center">
{fila.PMe === maxPMe && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Máximo
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-3">Relación entre PMg y PMe</h4>
<div className="space-y-3 text-sm text-gray-700">
<div className="flex items-start gap-3">
<ArrowRight className="w-4 h-4 text-purple-600 mt-0.5" />
<p>Cuando <strong>PMg {'>'} PMe</strong>, el producto medio está aumentando</p>
</div>
<div className="flex items-start gap-3">
<ArrowRight className="w-4 h-4 text-purple-600 mt-0.5" />
<p>Cuando <strong>PMg {'<'} PMe</strong>, el producto medio está disminuyendo</p>
</div>
<div className="flex items-start gap-3">
<ArrowRight className="w-4 h-4 text-purple-600 mt-0.5" />
<p>Cuando <strong>PMg = PMe</strong>, el producto medio está en su máximo</p>
</div>
</div>
</div>
</Card>
<Card>
<CardHeader
title="Pregunta de Análisis"
subtitle="Basado en los datos de la tabla"
/>
<div className="space-y-4">
<p className="text-gray-700">
<strong>Pregunta:</strong> ¿En qué nivel de trabajo (L) se alcanza el Producto Medio máximo
y cuál es su valor?
</p>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<div className="grid md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-green-700 mb-1">Nivel de trabajo (L):</p>
<p className="font-bold text-green-900 text-xl">{maxPMeL} trabajadores</p>
</div>
<div>
<p className="text-sm text-green-700 mb-1">Producto Medio máximo:</p>
<p className="font-bold text-green-900 text-xl">{maxPMe} unidades/trabajador</p>
</div>
</div>
</div>
<p className="text-sm text-gray-600">
<strong>Interpretación:</strong> Cada trabajador produce en promedio {maxPMe} unidades
cuando hay {maxPMeL} trabajadores. Este es el punto de máxima eficiencia por trabajador.
</p>
</div>
</Card>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
{!verificado ? (
<span>Completa todos los cálculos con 2 decimales</span>
) : (
<span>
Correctos: {datosCompletos.filter(f =>
Math.abs(parseFloat(respuestas[f.L]) - (f.PMe || 0)) < 0.1
).length} / {datosCompletos.length}
</span>
)}
</div>
<div className="flex gap-3">
{!verificado ? (
<Button onClick={handleVerificar} disabled={!todasRespondidas}>
Verificar Cálculos
</Button>
) : (
<>
<Button onClick={handleReiniciar} variant="outline">
Reiniciar
</Button>
{datosCompletos.filter(f =>
Math.abs(parseFloat(respuestas[f.L]) - (f.PMe || 0)) < 0.1
).length === datosCompletos.length && (
<Button onClick={() => onComplete?.(100)}>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</Button>
)}
</>
)}
</div>
</div>
</div>
);
}
export default ProductoMedio;

View File

@@ -0,0 +1,223 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, TrendingUp, AlertCircle } from 'lucide-react';
interface ProductoTotalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaProduccion {
L: number;
Q: number;
}
const datosProduccion: FilaProduccion[] = [
{ L: 0, Q: 0 },
{ L: 1, Q: 8 },
{ L: 2, Q: 20 },
{ L: 3, Q: 36 },
{ L: 4, Q: 52 },
{ L: 5, Q: 64 },
{ L: 6, Q: 72 },
{ L: 7, Q: 76 },
{ L: 8, Q: 76 },
{ L: 9, Q: 72 },
];
export function ProductoTotal({ ejercicioId: _ejercicioId, onComplete }: ProductoTotalProps) {
const [respuestaMax, setRespuestaMax] = useState('');
const [respuestaL, setRespuestaL] = useState('');
const [verificado, setVerificado] = useState(false);
const [correcto, setCorrecto] = useState({ max: false, l: false });
const maxQ = Math.max(...datosProduccion.map(d => d.Q));
const maxL = datosProduccion.find(d => d.Q === maxQ)?.L;
const handleVerificar = () => {
const esCorrectoMax = parseInt(respuestaMax) === maxQ;
const esCorrectoL = parseInt(respuestaL) === maxL;
setCorrecto({ max: esCorrectoMax, l: esCorrectoL });
setVerificado(true);
if (esCorrectoMax && esCorrectoL && onComplete) {
onComplete(100);
}
};
const handleReiniciar = () => {
setRespuestaMax('');
setRespuestaL('');
setVerificado(false);
setCorrecto({ max: false, l: false });
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Producto Total (PT)"
subtitle="Analiza el output máximo producido con diferentes niveles de trabajo"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Definición</span>
</div>
<p className="text-sm text-blue-700">
El <strong>Producto Total (PT o Q)</strong> es la cantidad total de output producida
utilizando una cierta cantidad de un factor variable (generalmente trabajo L),
manteniendo fijos los demás factores.
</p>
<p className="text-sm text-blue-600 mt-2">
<strong>Fórmula:</strong> PT = Q = f(L) cuando K es constante
</p>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-3 border text-left font-medium">Trabajo (L)</th>
<th className="px-4 py-3 border text-left font-medium">Producto Total (Q)</th>
<th className="px-4 py-3 border text-center font-medium">Estado</th>
</tr>
</thead>
<tbody>
{datosProduccion.map((fila, index) => (
<tr
key={fila.L}
className={`border-b ${
fila.Q === maxQ
? 'bg-green-50'
: index % 2 === 0
? 'bg-white'
: 'bg-gray-50'
}`}
>
<td className="px-4 py-3 border">{fila.L}</td>
<td className="px-4 py-3 border font-mono">{fila.Q}</td>
<td className="px-4 py-3 border text-center">
{fila.Q === maxQ && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Máximo
</span>
)}
{fila.L > 0 && fila.Q < datosProduccion[index - 1].Q && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Rendimientos negativos
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="w-5 h-5 text-yellow-600" />
<span className="font-semibold text-yellow-800">Análisis</span>
</div>
<ul className="text-sm text-yellow-700 space-y-1 ml-5 list-disc">
<li>La producción aumenta hasta cierto punto (L = 7 u 8)</li>
<li>Beyond that point, los rendimientos son decrecientes</li>
<li>Con L = 9, el producto total disminuye (rendimientos negativos)</li>
</ul>
</div>
</Card>
<Card>
<CardHeader
title="Ejercicio de Cálculo"
subtitle="Responde basándote en la tabla anterior"
/>
<div className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
¿Cuál es el Producto Total máximo?
</label>
<Input
type="number"
value={respuestaMax}
onChange={(e) => setRespuestaMax(e.target.value)}
placeholder="Valor de Q máximo"
disabled={verificado}
className={verificado
? correcto.max
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}
/>
{verificado && (
<p className={`text-sm ${correcto.max ? 'text-success' : 'text-error'}`}>
{correcto.max ? '✓ Correcto' : `✗ Incorrecto. La respuesta es ${maxQ}`}
</p>
)}
</div>
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
¿Con cuántos trabajadores (L) se alcanza este máximo?
</label>
<Input
type="number"
value={respuestaL}
onChange={(e) => setRespuestaL(e.target.value)}
placeholder="Valor de L"
disabled={verificado}
className={verificado
? correcto.l
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}
/>
{verificado && (
<p className={`text-sm ${correcto.l ? 'text-success' : 'text-error'}`}>
{correcto.l ? '✓ Correcto' : `✗ Incorrecto. La respuesta es ${maxL}`}
</p>
)}
</div>
</div>
<div className="flex gap-3">
{!verificado ? (
<Button
onClick={handleVerificar}
disabled={!respuestaMax || !respuestaL}
>
Verificar Respuestas
</Button>
) : (
<>
<Button onClick={handleReiniciar} variant="outline">
Intentar de Nuevo
</Button>
{(correcto.max && correcto.l) && (
<Button
onClick={() => onComplete?.(100)}
variant="primary"
>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</Button>
)}
</>
)}
</div>
</div>
</Card>
</div>
);
}
export default ProductoTotal;

View File

@@ -0,0 +1,199 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Brain } from 'lucide-react';
export function ProductorRacional() {
const [respuestas, setRespuestas] = useState<{[key: string]: boolean | null}>({
afirmacion1: null,
afirmacion2: null,
afirmacion3: null,
afirmacion4: null,
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const afirmaciones = [
{
id: 'afirmacion1',
texto: 'Un productor racional siempre busca minimizar costos para un nivel dado de producción.',
esCorrecta: true,
explicacion: 'Correcto. La racionalidad económica implica optimizar recursos, lo que incluye minimizar costos para producir una cantidad determinada.'
},
{
id: 'afirmacion2',
texto: 'Producir en la Etapa III es racional si los precios son muy altos.',
esCorrecta: false,
explicacion: 'Incorrecto. En la Etapa III el producto marginal es negativo, por lo que producir más disminuye el output total. Nunca es racional operar aquí.'
},
{
id: 'afirmacion3',
texto: 'El productor racional equilibra el ingreso marginal con el costo marginal.',
esCorrecta: true,
explicacion: 'Correcto. La condición de maximización de beneficios es IMg = CMg. Producir donde el ingreso adicional iguala al costo adicional.'
},
{
id: 'afirmacion4',
texto: 'Producir en la Etapa I es óptimo porque los rendimientos son crecientes.',
esCorrecta: false,
explicacion: 'Incorrecto. Aunque los rendimientos son crecientes en la Etapa I, el productor puede aumentar la producción y los beneficios moviéndose a la Etapa II.'
}
];
const seleccionarRespuesta = (id: string, valor: boolean) => {
setRespuestas(prev => ({ ...prev, [id]: valor }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasRespondidas = Object.values(respuestas).every(r => r !== null);
const correctas = afirmaciones.filter(a => respuestas[a.id] === a.esCorrecta).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="El Productor Racional"
subtitle="Determina qué afirmaciones describen correctamente el comportamiento de un productor racional"
/>
<div className="space-y-6">
{/* Diagrama de decisión */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4 text-center">Zona de Decisión del Productor</h4>
<svg className="w-full h-56" viewBox="0 0 500 200">
{/* Ejes */}
<line x1="50" y1="170" x2="450" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="170" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="250" y="195" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad de Trabajo</text>
<text x="20" y="95" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 20 95)">PT</text>
{/* Curva PT */}
<path
d="M 50,170 Q 150,130 250,100 Q 350,70 400,90 Q 430,110 450,150"
fill="none"
stroke="#374151"
strokeWidth="2"
/>
{/* Zona I */}
<rect x="50" y="20" width="100" height="150" fill="#dcfce7" opacity="0.6" />
<text x="100" y="40" textAnchor="middle" className="text-xs font-bold fill-green-700">ZONA I</text>
<text x="100" y="55" textAnchor="middle" className="text-xs fill-green-600">No óptima</text>
{/* Zona II - ZONA RACIONAL */}
<rect x="150" y="20" width="200" height="150" fill="#dbeafe" opacity="0.8" stroke="#2563eb" strokeWidth="3" strokeDasharray="8" />
<text x="250" y="45" textAnchor="middle" className="text-base font-bold fill-blue-700">ZONA RACIONAL</text>
<text x="250" y="65" textAnchor="middle" className="text-xs fill-blue-600">ETAPA II</text>
<text x="250" y="80" textAnchor="middle" className="text-xs fill-blue-600">Donde opera el</text>
<text x="250" y="95" textAnchor="middle" className="text-xs fill-blue-600">productor eficiente</text>
{/* Zona III */}
<rect x="350" y="20" width="100" height="150" fill="#fee2e2" opacity="0.6" />
<text x="400" y="40" textAnchor="middle" className="text-xs font-bold fill-red-700">ZONA III</text>
<text x="400" y="55" textAnchor="middle" className="text-xs fill-red-600">Irracional</text>
{/* Límites */}
<line x1="150" y1="20" x2="150" y2="170" stroke="#22c55e" strokeWidth="2" />
<line x1="350" y1="20" x2="350" y2="170" stroke="#ef4444" strokeWidth="2" />
</svg>
</div>
{/* Afirmaciones */}
<div className="space-y-4">
{afirmaciones.map((afirmacion, index) => (
<div key={afirmacion.id} className="bg-white border rounded-lg p-4">
<div className="flex items-start gap-3 mb-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-gray-100 text-gray-700 flex items-center justify-center text-sm font-bold">
{index + 1}
</span>
<p className="text-gray-800">{afirmacion.texto}</p>
</div>
<div className="flex gap-3 ml-9">
<button
onClick={() => seleccionarRespuesta(afirmacion.id, true)}
disabled={mostrarResultados}
className={`flex-1 p-2 rounded-lg border-2 text-center transition-all ${
respuestas[afirmacion.id] === true && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && afirmacion.esCorrecta === true
? 'border-green-500 bg-green-50'
: mostrarResultados && respuestas[afirmacion.id] === true && afirmacion.esCorrecta === false
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">VERDADERO</span>
{mostrarResultados && afirmacion.esCorrecta && (
<CheckCircle className="w-4 h-4 text-green-600 mx-auto mt-1" />
)}
{mostrarResultados && respuestas[afirmacion.id] === true && !afirmacion.esCorrecta && (
<XCircle className="w-4 h-4 text-red-600 mx-auto mt-1" />
)}
</button>
<button
onClick={() => seleccionarRespuesta(afirmacion.id, false)}
disabled={mostrarResultados}
className={`flex-1 p-2 rounded-lg border-2 text-center transition-all ${
respuestas[afirmacion.id] === false && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && afirmacion.esCorrecta === false
? 'border-green-500 bg-green-50'
: mostrarResultados && respuestas[afirmacion.id] === false && afirmacion.esCorrecta === true
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">FALSO</span>
{mostrarResultados && !afirmacion.esCorrecta && (
<CheckCircle className="w-4 h-4 text-green-600 mx-auto mt-1" />
)}
{mostrarResultados && respuestas[afirmacion.id] === false && afirmacion.esCorrecta && (
<XCircle className="w-4 h-4 text-red-600 mx-auto mt-1" />
)}
</button>
</div>
{mostrarResultados && (
<div className={`mt-3 p-3 rounded text-sm ${respuestas[afirmacion.id] === afirmacion.esCorrecta ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'}`}>
{afirmacion.explicacion}
</div>
)}
</div>
))}
</div>
<Button onClick={validar} disabled={!todasRespondidas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 4 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
<Brain className="w-5 h-5 text-gray-700" />
<span className="font-semibold">Resultado: {correctas}/4 correctas</span>
</div>
{correctas === 4 && (
<p className="text-sm text-green-700">
¡Excelente! Comprendes perfectamente qué hace racional a un productor.
</p>
)}
{correctas < 4 && (
<p className="text-sm text-amber-700">
Revisa las explicaciones para entender mejor el comportamiento del productor racional.
</p>
)}
</div>
)}
</div>
</Card>
</div>
);
}
export default ProductorRacional;

View File

@@ -0,0 +1,310 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Power, RotateCcw, AlertTriangle, Calculator } from 'lucide-react';
interface PuntoCierreEquilibrioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
nombre: string;
precio: number;
q: number;
cf: number;
cv: number;
descripcion: string;
}
export function PuntoCierreEquilibrio({ ejercicioId: _ejercicioId, onComplete }: PuntoCierreEquilibrioProps) {
const escenarios: Escenario[] = [
{ nombre: 'Beneficios', precio: 60, q: 100, cf: 2000, cv: 3000, descripcion: 'P > CMe: La empresa gana dinero' },
{ nombre: 'Equilibrio', precio: 50, q: 100, cf: 2000, cv: 3000, descripcion: 'P = CMe: Beneficio = 0 (normal)' },
{ nombre: 'Pérdida pero opera', precio: 35, q: 100, cf: 2000, cv: 3000, descripcion: 'CVMe < P < CMe: Cubre CV, parte de CF' },
{ nombre: 'Punto de cierre', precio: 30, q: 100, cf: 2000, cv: 3000, descripcion: 'P = CVMe: Debe cerrar a largo plazo' },
{ nombre: 'Cierre inmediato', precio: 25, q: 100, cf: 2000, cv: 3000, descripcion: 'P < CVMe: Debe cerrar inmediatamente' },
];
const [escenarioSeleccionado, setEscenarioSeleccionado] = useState(0);
const [respuestas, setRespuestas] = useState({
ingresoTotal: '',
costoTotal: '',
costoVariable: '',
beneficio: '',
decision: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const escenario = escenarios[escenarioSeleccionado];
const calculos = useMemo(() => {
const it = escenario.precio * escenario.q;
const ct = escenario.cf + escenario.cv;
const cvme = escenario.cv / escenario.q;
const cme = ct / escenario.q;
const beneficio = it - ct;
return { it, ct, cvme, cme, beneficio };
}, [escenario]);
const decisionCorrecta = useMemo(() => {
if (calculos.beneficio >= 0) return 'producir';
if (escenario.precio > calculos.cvme) return 'producir_perdida';
return 'cerrar';
}, [calculos, escenario.precio]);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseFloat(respuestas.ingresoTotal) !== calculos.it) {
nuevosErrores.push(`IT incorrecto. IT = P × Q = ${escenario.precio} × ${escenario.q}`);
}
if (parseFloat(respuestas.costoTotal) !== calculos.ct) {
nuevosErrores.push(`CT incorrecto. CT = CF + CV = ${escenario.cf} + ${escenario.cv}`);
}
if (parseFloat(respuestas.costoVariable) !== escenario.cv) {
nuevosErrores.push(`CV incorrecto. El CV es ${escenario.cv}`);
}
if (parseFloat(respuestas.beneficio) !== calculos.beneficio) {
nuevosErrores.push(`Beneficio incorrecto. Beneficio = IT - CT`);
}
const respDecision = respuestas.decision.toLowerCase().trim();
const esCorrecto =
(decisionCorrecta === 'producir' && (respDecision.includes('producir') || respDecision.includes('continuar'))) ||
(decisionCorrecta === 'producir_perdida' && (respDecision.includes('producir') || respDecision.includes('operar'))) ||
(decisionCorrecta === 'cerrar' && (respDecision.includes('cerrar') || respDecision.includes('parar')));
if (!esCorrecto) {
if (decisionCorrecta === 'producir') {
nuevosErrores.push('La empresa debe seguir produciendo porque obtiene beneficios.');
} else if (decisionCorrecta === 'producir_perdida') {
nuevosErrores.push('La empresa debe seguir produciendo en el corto plazo porque P > CVMe (cubre los costos variables).');
} else {
nuevosErrores.push('La empresa debe cerrar porque P < CVMe (no cubre los costos variables).');
}
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({ ingresoTotal: '', costoTotal: '', costoVariable: '', beneficio: '', decision: '' });
setValidado(false);
setErrores([]);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Punto de Cierre y Equilibrio"
subtitle="Decisiones de producción en el corto plazo"
/>
<div className="bg-orange-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Power className="w-5 h-5 text-orange-600" />
<span className="font-semibold text-orange-800">Reglas de Decisión</span>
</div>
<p className="text-sm text-orange-700">
<strong>Punto de cierre:</strong> Si P {'<'} CVMe, la empresa debe cerrar inmediatamente
porque ni siquiera cubre los costos variables. <strong>Equilibrio:</strong> Si P = CMe,
la empresa obtiene beneficio cero (beneficio normal).
</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">Selecciona un escenario:</label>
<select
value={escenarioSeleccionado}
onChange={(e) => {
setEscenarioSeleccionado(parseInt(e.target.value));
reiniciar();
}}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
>
{escenarios.map((e, i) => (
<option key={i} value={i}>{e.nombre}</option>
))}
</select>
</div>
<div className={`p-4 rounded-lg mb-6 ${
escenarioSeleccionado === 0 ? 'bg-green-50 border-2 border-green-300' :
escenarioSeleccionado === 1 ? 'bg-blue-50 border-2 border-blue-300' :
escenarioSeleccionado === 2 ? 'bg-yellow-50 border-2 border-yellow-300' :
escenarioSeleccionado === 3 ? 'bg-orange-50 border-2 border-orange-300' :
'bg-red-50 border-2 border-red-300'
}`}>
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className={`w-5 h-5 ${
escenarioSeleccionado <= 1 ? 'text-green-600' :
escenarioSeleccionado === 2 ? 'text-yellow-600' :
'text-red-600'
}`} />
<span className={`font-semibold ${
escenarioSeleccionado <= 1 ? 'text-green-800' :
escenarioSeleccionado === 2 ? 'text-yellow-800' :
'text-red-800'
}`}>
{escenario.nombre}
</span>
</div>
<p className="text-sm text-gray-700">{escenario.descripcion}</p>
</div>
<div className="grid md:grid-cols-5 gap-4 mb-6">
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Precio (P)</p>
<p className="text-xl font-bold text-primary">${escenario.precio}</p>
</div>
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Cantidad (Q)</p>
<p className="text-xl font-bold text-secondary">{escenario.q}</p>
</div>
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Costo Fijo (CF)</p>
<p className="text-xl font-bold text-gray-700">${escenario.cf}</p>
</div>
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Costo Variable (CV)</p>
<p className="text-xl font-bold text-gray-700">${escenario.cv}</p>
</div>
<div className={`p-3 rounded-lg text-center border-2 ${
calculos.beneficio >= 0 ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}`}>
<p className="text-xs text-gray-600 mb-1">CMe ($)</p>
<p className="text-xl font-bold">{calculos.cme.toFixed(2)}</p>
</div>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5 text-blue-600" />
Completa los cálculos:
</h4>
<div className="grid md:grid-cols-5 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Ingreso Total ($)</label>
<Input
type="number"
value={respuestas.ingresoTotal}
onChange={(e) => handleRespuestaChange('ingresoTotal', e.target.value)}
className="w-full"
placeholder="P × Q"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Costo Total ($)</label>
<Input
type="number"
value={respuestas.costoTotal}
onChange={(e) => handleRespuestaChange('costoTotal', e.target.value)}
className="w-full"
placeholder="CF + CV"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">CV Total ($)</label>
<Input
type="number"
value={respuestas.costoVariable}
onChange={(e) => handleRespuestaChange('costoVariable', e.target.value)}
className="w-full"
placeholder="CV"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Beneficio ($)</label>
<Input
type="number"
value={respuestas.beneficio}
onChange={(e) => handleRespuestaChange('beneficio', e.target.value)}
className="w-full"
placeholder="IT - CT"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Decisión</label>
<Input
type="text"
value={respuestas.decision}
onChange={(e) => handleRespuestaChange('decision', e.target.value)}
className="w-full"
placeholder="Producir / Cerrar"
/>
</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" />
Limpiar
</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! Respuestas validadas</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>
<div className="grid md:grid-cols-2 gap-6">
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Punto de Equilibrio:</h4>
<ul className="space-y-2 text-sm text-green-800">
<li> <strong>Definición:</strong> Cuando P = CMe (Beneficio = 0)</li>
<li> <strong>Significado:</strong> La empresa cubre todos sus costos</li>
<li> <strong>Beneficio:</strong> Es el beneficio "normal" del empresario</li>
<li> <strong>Decisión:</strong> Continuar operando</li>
</ul>
</Card>
<Card className="bg-red-50 border-red-200">
<h4 className="font-semibold text-red-900 mb-2">Punto de Cierre:</h4>
<ul className="space-y-2 text-sm text-red-800">
<li> <strong>Definición:</strong> Cuando P = CVMe mínimo</li>
<li> <strong>Si P {'>'} CVMe:</strong> Cubre CV, ayuda con CF Seguir produciendo</li>
<li> <strong>Si P = CVMe:</strong> Indiferente entre producir o cerrar</li>
<li> <strong>Si P {'<'} CVMe:</strong> Ni siquiera cubre CV Cerrar inmediatamente</li>
</ul>
</Card>
</div>
</div>
);
}
export default PuntoCierreEquilibrio;

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, Target, RotateCcw, Calculator } from 'lucide-react';
interface ReglaImgCmgProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface DatoMercado {
q: number;
cmg: number;
img: number;
ct: number;
it: number;
}
export function ReglaImgCmg({ ejercicioId: _ejercicioId, onComplete }: ReglaImgCmgProps) {
const datosMercado: DatoMercado[] = [
{ q: 0, cmg: 0, img: 100, ct: 50, it: 0 },
{ q: 1, cmg: 30, img: 90, ct: 80, it: 90 },
{ q: 2, cmg: 40, img: 80, ct: 120, it: 160 },
{ q: 3, cmg: 50, img: 70, ct: 170, it: 210 },
{ q: 4, cmg: 60, img: 60, ct: 230, it: 240 },
{ q: 5, cmg: 70, img: 50, ct: 300, it: 250 },
{ q: 6, cmg: 80, img: 40, ct: 380, it: 240 },
{ q: 7, cmg: 90, img: 30, ct: 470, it: 210 },
{ q: 8, cmg: 100, img: 20, ct: 570, it: 160 },
];
const [respuestas, setRespuestas] = useState({
qOptima: '',
beneficio: '',
condicion: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const qOptima = useMemo(() => {
const datoOptimo = datosMercado
.filter(d => d.cmg <= d.img && d.q > 0)
.pop();
return datoOptimo?.q || 0;
}, []);
const beneficioMaximo = useMemo(() => {
const datoOptimo = datosMercado.find(d => d.q === qOptima);
return datoOptimo ? datoOptimo.it - datoOptimo.ct : 0;
}, [qOptima]);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseInt(respuestas.qOptima) !== qOptima) {
nuevosErrores.push(`La cantidad óptima no es correcta. Busca donde IMg = CMg (o IMg >= CMg más cercano)`);
}
if (parseFloat(respuestas.beneficio) !== beneficioMaximo) {
nuevosErrores.push(`El beneficio máximo es incorrecto. Beneficio = IT - CT`);
}
if (!respuestas.condicion.toLowerCase().includes('img = cmg') &&
!respuestas.condicion.toLowerCase().includes('img igual a cmg') &&
!respuestas.condicion.toLowerCase().includes('ingreso marginal igual a costo marginal')) {
nuevosErrores.push('La condición de maximización es IMg = CMg');
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({ qOptima: '', beneficio: '', condicion: '' });
setValidado(false);
setErrores([]);
};
const maxValor = Math.max(...datosMercado.map(d => Math.max(d.cmg, d.img)));
const escalaY = 100 / maxValor;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Regla de Maximización de Beneficios"
subtitle="IMg = CMg - Producción óptima"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Target className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Regla Fundamental</span>
</div>
<p className="text-sm text-blue-700">
Una empresa maximiza su beneficio cuando produce la cantidad donde el <strong>Ingreso Marginal (IMg)
es igual al Costo Marginal (CMg)</strong>. Si IMg {'>'} CMg, debe producir más. Si IMg {'<'} CMg,
debe producir menos.
</p>
</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)">Costo/Ingreso ($)</text>
{datosMercado.map((d, i) => (
<g key={i}>
<line x1={50 + i * 35} y1="160" x2={50 + i * 35} y2="165" stroke="#374151" strokeWidth="1" />
<text x={50 + i * 35} y="175" textAnchor="middle" className="text-xs fill-gray-500">{d.q}</text>
</g>
))}
<polyline
fill="none"
stroke="#dc2626"
strokeWidth="3"
points={datosMercado
.filter(d => d.q > 0)
.map((d, i) => `${85 + i * 35},${160 - d.cmg * escalaY}`)
.join(' ')}
/>
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="3"
points={datosMercado
.filter(d => d.q > 0)
.map((d, i) => `${85 + i * 35},${160 - d.img * escalaY}`)
.join(' ')}
/>
<circle
cx={50 + 4 * 35}
cy={160 - 60 * escalaY}
r="8"
fill="#10b981"
stroke="white"
strokeWidth="3"
/>
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#dc2626" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">CMg</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#2563eb" strokeWidth="2" />
<text x="25" y="19" className="text-xs fill-gray-600">IMg</text>
</g>
<text x={50 + 4 * 35} y={130 - 60 * escalaY} textAnchor="middle" className="text-xs fill-green-700 font-bold">
Q* = {qOptima}
</text>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 text-red-600">CMg ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 text-blue-600">IMg ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Beneficio ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Decisión</th>
</tr>
</thead>
<tbody>
{datosMercado.map((d) => {
const beneficio = d.it - d.ct;
const esOptimo = d.q === qOptima;
const debeExpandir = d.img > d.cmg && d.q > 0;
const debeReducir = d.img < d.cmg;
return (
<tr
key={d.q}
className={`border-b hover:bg-gray-50 ${esOptimo ? 'bg-green-50' : ''}`}
>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2 text-red-600">{d.cmg || '-'}</td>
<td className="px-3 py-2 text-blue-600">{d.img}</td>
<td className="px-3 py-2">{d.ct}</td>
<td className="px-3 py-2">{d.it}</td>
<td className={`px-3 py-2 font-medium ${beneficio >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{d.q === 0 ? '-' : beneficio}
</td>
<td className="px-3 py-2">
{d.q === 0 ? '-' : (
<span className={`text-xs px-2 py-1 rounded ${
esOptimo ? 'bg-green-200 text-green-800' :
debeExpandir ? 'bg-blue-100 text-blue-800' :
'bg-red-100 text-red-800'
}`}>
{esOptimo ? 'ÓPTIMO ✓' : debeExpandir ? 'Expandir ↑' : 'Reducir ↓'}
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5 text-blue-600" />
Responde:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cantidad óptima (Q*)?
</label>
<Input
type="number"
value={respuestas.qOptima}
onChange={(e) => handleRespuestaChange('qOptima', e.target.value)}
className="w-full"
placeholder="Busca IMg = CMg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Beneficio máximo? ($)
</label>
<Input
type="number"
value={respuestas.beneficio}
onChange={(e) => handleRespuestaChange('beneficio', e.target.value)}
className="w-full"
placeholder="IT - CT"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Condición de maximización?
</label>
<Input
type="text"
value={respuestas.condicion}
onChange={(e) => handleRespuestaChange('condicion', e.target.value)}
className="w-full"
placeholder="IMg = CMg"
/>
</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! La empresa maximiza beneficios con Q* = {qOptima}, obteniendo un beneficio de ${beneficioMaximo}
</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-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Reglas de Maximización:</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> <strong>IMg {'>'} CMg:</strong> La empresa debe aumentar la producción</li>
<li> <strong>IMg {'<'} CMg:</strong> La empresa debe reducir la producción</li>
<li> <strong>IMg = CMg:</strong> La empresa está maximizando beneficios</li>
<li> <strong>Beneficio = IT - CT</strong> (o también: (P - CMe) × Q)</li>
</ul>
</Card>
</div>
);
}
export default ReglaImgCmg;

View File

@@ -0,0 +1,235 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, GitCompare } from 'lucide-react';
export function RelacionCMgCMe() {
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({
pregunta1: '',
pregunta2: '',
pregunta3: '',
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const preguntas = [
{
id: 'pregunta1',
texto: 'Cuando CMg < CMe, el costo medio:',
opciones: [
{ id: 'a', texto: 'Aumenta', correcta: false },
{ id: 'b', texto: 'Disminuye', correcta: true },
{ id: 'c', texto: 'Se mantiene constante', correcta: false },
],
explicacion: 'Si el costo marginal es menor que el costo medio, "arrastra" el promedio hacia abajo, haciendo que CMe disminuya.'
},
{
id: 'pregunta2',
texto: 'El CMe alcanza su mínimo cuando:',
opciones: [
{ id: 'a', texto: 'CMg = 0', correcta: false },
{ id: 'b', texto: 'CMg es máximo', correcta: false },
{ id: 'c', texto: 'CMg = CMe', correcta: true },
],
explicacion: 'El CMg corta a CMe en su punto mínimo. Cuando se igualan, CMe deja de caer y empieza a subir.'
},
{
id: 'pregunta3',
texto: 'Cuando CMg > CMe, el costo medio:',
opciones: [
{ id: 'a', texto: 'Aumenta', correcta: true },
{ id: 'b', texto: 'Disminuye', correcta: false },
{ id: 'c', texto: 'Es cero', correcta: false },
],
explicacion: 'Si el costo marginal es mayor que el costo medio, "empuja" el promedio hacia arriba, haciendo que CMe aumente.'
}
];
const seleccionarRespuesta = (preguntaId: string, opcionId: string) => {
setRespuestas(prev => ({ ...prev, [preguntaId]: opcionId }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasRespondidas = Object.values(respuestas).every(r => r !== '');
const esCorrecta = (preguntaId: string) => {
const pregunta = preguntas.find(p => p.id === preguntaId);
return pregunta?.opciones.find(o => o.id === respuestas[preguntaId])?.correcta || false;
};
const correctas = preguntas.filter(p => esCorrecta(p.id)).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Relación entre CMg y CMe"
subtitle="Comprende cómo el costo marginal afecta al costo medio"
/>
<div className="space-y-6">
{/* Gráfico animado */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4 text-center">CMg "jalona" al CMe</h4>
<svg className="w-full h-64" viewBox="0 0 600 250">
{/* Ejes */}
<line x1="60" y1="220" x2="550" y2="220" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="220" x2="60" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="305" y="245" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
<text x="25" y="120" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 25 120)">Costo ($)</text>
{/* Curva CMe */}
<path
d="M 90,180 Q 200,80 300,70 Q 400,80 500,160"
fill="none"
stroke="#7c3aed"
strokeWidth="3"
/>
{/* Curva CMg */}
<path
d="M 90,220 Q 180,120 250,70 Q 320,60 380,100 Q 450,180 520,200"
fill="none"
stroke="#16a34a"
strokeWidth="3"
strokeDasharray="5"
/>
{/* Punto de corte */}
<circle cx="300" cy="70" r="8" fill="#ef4444" stroke="white" strokeWidth="3" />
<text x="320" y="60" className="text-sm fill-red-600 font-bold">Mínimo CMe</text>
<text x="300" y="85" textAnchor="middle" className="text-xs fill-red-600">CMg = CMe</text>
{/* Zona 1: CMg < CMe */}
<rect x="90" y="20" width="210" height="200" fill="#22c55e" opacity="0.1" />
<text x="195" y="40" textAnchor="middle" className="text-sm font-bold fill-green-700">ZONA 1</text>
<text x="195" y="60" textAnchor="middle" className="text-xs fill-green-600">CMg {'<'} CMe</text>
<text x="195" y="75" textAnchor="middle" className="text-xs fill-green-600">CMe decrece</text>
{/* Zona 2: CMg > CMe */}
<rect x="300" y="20" width="250" height="200" fill="#ef4444" opacity="0.1" />
<text x="425" y="40" textAnchor="middle" className="text-sm font-bold fill-red-700">ZONA 2</text>
<text x="425" y="60" textAnchor="middle" className="text-xs fill-red-600">CMg {'>'} CMe</text>
<text x="425" y="75" textAnchor="middle" className="text-xs fill-red-600">CMe aumenta</text>
{/* Flechas indicadoras */}
<path d="M 150,100 L 150,130" stroke="#22c55e" strokeWidth="2" markerEnd="url(#arrowGreen)" />
<path d="M 450,140 L 450,110" stroke="#ef4444" strokeWidth="2" markerEnd="url(#arrowRed)" />
<defs>
<marker id="arrowGreen" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#22c55e" />
</marker>
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#ef4444" />
</marker>
</defs>
{/* Leyenda */}
<g transform="translate(400, 180)">
<line x1="0" y1="0" x2="30" y2="0" stroke="#7c3aed" strokeWidth="3" />
<text x="35" y="4" className="text-sm fill-gray-700">CMe</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#16a34a" strokeWidth="3" strokeDasharray="4" />
<text x="35" y="24" className="text-sm fill-gray-700">CMg</text>
</g>
</svg>
</div>
{/* Analogía */}
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Analogía del Promedio y la Nueva Nota</h4>
<p className="text-sm text-blue-800">
Imagina tu promedio académico (<strong>CMe</strong>) y tu próxima nota (<strong>CMg</strong>):
</p>
<ul className="list-disc list-inside text-sm text-blue-700 mt-2 space-y-1">
<li>Si tu nueva nota (CMg) es <strong>menor</strong> que tu promedio (CMe) tu promedio <strong>baja</strong></li>
<li>Si tu nueva nota (CMg) es <strong>igual</strong> a tu promedio (CMe) tu promedio <strong>se mantiene</strong> (mínimo)</li>
<li>Si tu nueva nota (CMg) es <strong>mayor</strong> que tu promedio (CMe) tu promedio <strong>sube</strong></li>
</ul>
</div>
{/* Preguntas */}
<div className="space-y-4">
{preguntas.map((pregunta) => {
const preguntaCorrecta = esCorrecta(pregunta.id);
return (
<div key={pregunta.id} className="bg-white border rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-3">{pregunta.texto}</h4>
<div className="space-y-2">
{pregunta.opciones.map((opcion) => {
const esSeleccionada = respuestas[pregunta.id] === opcion.id;
const mostrarCorrecta = mostrarResultados && opcion.correcta;
const mostrarIncorrecta = mostrarResultados && esSeleccionada && !opcion.correcta;
return (
<button
key={opcion.id}
onClick={() => seleccionarRespuesta(pregunta.id, opcion.id)}
disabled={mostrarResultados}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
esSeleccionada && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarCorrecta
? 'border-green-500 bg-green-50'
: mostrarIncorrecta
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">{opcion.id})</span> {opcion.texto}
{mostrarCorrecta && <CheckCircle className="w-5 h-5 text-green-600 inline ml-2" />}
{mostrarIncorrecta && <XCircle className="w-5 h-5 text-red-600 inline ml-2" />}
</button>
);
})}
</div>
{mostrarResultados && (
<div className={`mt-3 p-3 rounded text-sm ${preguntaCorrecta ? 'bg-green-50 text-green-800' : 'bg-amber-50 text-amber-800'}`}>
{pregunta.explicacion}
</div>
)}
</div>
);
})}
</div>
<Button onClick={validar} disabled={!todasRespondidas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 3 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
<GitCompare className="w-5 h-5" />
<span className="font-semibold">Resultado: {correctas}/3 correctas</span>
</div>
{correctas === 3 && (
<p className="text-sm text-green-700">¡Excelente! Dominas la relación entre CMg y CMe.</p>
)}
</div>
)}
</div>
</Card>
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Regla de Oro</h4>
<div className="space-y-2 text-sm text-green-800">
<p><strong>CMg {'<'} CMe CMe decrece</strong> (costo marginal menor que el promedio)</p>
<p><strong>CMg = CMe CMe mínimo</strong> (punto de eficiencia)</p>
<p><strong>CMg {'>'} CMe CMe crece</strong> (costo marginal mayor que el promedio)</p>
</div>
</Card>
</div>
);
}
export default RelacionCMgCMe;

View File

@@ -0,0 +1,200 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Table } from 'lucide-react';
interface FilaCostos {
q: number;
cf: number;
cv: number;
ct: number | null;
cme: number | null;
cmg: number | null;
}
export function TablaCostos() {
const CF_BASE = 200;
const [filas, setFilas] = useState<FilaCostos[]>([
{ q: 0, cf: CF_BASE, cv: 0, ct: null, cme: null, cmg: null },
{ q: 1, cf: CF_BASE, cv: 50, ct: null, cme: null, cmg: null },
{ q: 2, cf: CF_BASE, cv: 90, ct: null, cme: null, cmg: null },
{ q: 3, cf: CF_BASE, cv: 120, ct: null, cme: null, cmg: null },
{ q: 4, cf: CF_BASE, cv: 160, ct: null, cme: null, cmg: null },
{ q: 5, cf: CF_BASE, cv: 220, ct: null, cme: null, cmg: null },
{ q: 6, cf: CF_BASE, cv: 300, ct: null, cme: null, cmg: null },
{ q: 7, cf: CF_BASE, cv: 400, ct: null, cme: null, cmg: null },
{ q: 8, cf: CF_BASE, cv: 520, ct: null, cme: null, cmg: null },
]);
const [mostrarResultados, setMostrarResultados] = useState(false);
const handleInputChange = (index: number, campo: 'ct' | 'cme' | 'cmg', valor: string) => {
const numValor = valor === '' ? null : parseFloat(valor);
const nuevasFilas = [...filas];
nuevasFilas[index] = { ...nuevasFilas[index], [campo]: numValor };
setFilas(nuevasFilas);
setMostrarResultados(false);
};
// Valores correctos
const valoresCorrectos = [
{ ct: 200, cme: null, cmg: null },
{ ct: 250, cme: 250, cmg: 50 },
{ ct: 290, cme: 145, cmg: 40 },
{ ct: 320, cme: 106.67, cmg: 30 },
{ ct: 360, cme: 90, cmg: 40 },
{ ct: 420, cme: 84, cmg: 60 },
{ ct: 500, cme: 83.33, cmg: 80 },
{ ct: 600, cme: 85.71, cmg: 100 },
{ ct: 720, cme: 90, cmg: 120 },
];
const validar = () => {
setMostrarResultados(true);
};
const esCorrecto = (index: number, campo: 'ct' | 'cme' | 'cmg', valor: number | null) => {
if (valor === null) return false;
const correcto = valoresCorrectos[index][campo];
if (correcto === null) return true;
if (campo === 'cme' && index > 0) {
return Math.abs(valor - correcto) < 1;
}
return valor === correcto;
};
const todasCompletadas = filas.every((fila, index) => {
if (index === 0) return fila.ct !== null;
return fila.ct !== null && fila.cme !== null && fila.cmg !== null;
});
const calcularCorrectas = () => {
let correctas = 0;
filas.forEach((fila, index) => {
if (esCorrecto(index, 'ct', fila.ct)) correctas++;
if (index > 0) {
if (esCorrecto(index, 'cme', fila.cme)) correctas++;
if (esCorrecto(index, 'cmg', fila.cmg)) correctas++;
}
});
return correctas;
};
const totalCampos = 1 + (filas.length - 1) * 3;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Tabla Completa de Costos"
subtitle="Completa la tabla calculando CT, CMe y CMg. CF = $200 (constante)"
/>
<div className="space-y-6">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-2 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-2 py-2 text-left font-medium text-gray-700">CF</th>
<th className="px-2 py-2 text-left font-medium text-gray-700">CV</th>
<th className="px-2 py-2 text-left font-medium text-gray-700 bg-blue-50">CT</th>
<th className="px-2 py-2 text-left font-medium text-gray-700 bg-green-50">CMe</th>
<th className="px-2 py-2 text-left font-medium text-gray-700 bg-amber-50">CMg</th>
</tr>
</thead>
<tbody>
{filas.map((fila, index) => (
<tr key={index} className="border-b">
<td className="px-2 py-2 font-medium">{fila.q}</td>
<td className="px-2 py-2 text-gray-600">${fila.cf}</td>
<td className="px-2 py-2 text-gray-600">${fila.cv}</td>
<td className={`px-2 py-2 bg-blue-50 ${mostrarResultados ? (esCorrecto(index, 'ct', fila.ct) ? 'bg-green-100' : 'bg-red-100') : ''}`}>
<div className="flex items-center gap-1">
<span>$</span>
<input
type="number"
value={fila.ct ?? ''}
onChange={(e) => handleInputChange(index, 'ct', e.target.value)}
className="w-16 px-1 py-1 border rounded text-sm"
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto(index, 'ct', fila.ct) && <CheckCircle className="w-4 h-4 text-green-600" />}
{mostrarResultados && !esCorrecto(index, 'ct', fila.ct) && <XCircle className="w-4 h-4 text-red-600" />}
</div>
</td>
<td className={`px-2 py-2 bg-green-50 ${mostrarResultados && index > 0 ? (esCorrecto(index, 'cme', fila.cme) ? 'bg-green-100' : 'bg-red-100') : ''}`}>
{index === 0 ? (
<span className="text-gray-400">-</span>
) : (
<div className="flex items-center gap-1">
<span>$</span>
<input
type="number"
value={fila.cme ?? ''}
onChange={(e) => handleInputChange(index, 'cme', e.target.value)}
className="w-16 px-1 py-1 border rounded text-sm"
disabled={mostrarResultados}
step="0.01"
/>
{mostrarResultados && esCorrecto(index, 'cme', fila.cme) && <CheckCircle className="w-4 h-4 text-green-600" />}
{mostrarResultados && !esCorrecto(index, 'cme', fila.cme) && <XCircle className="w-4 h-4 text-red-600" />}
</div>
)}
</td>
<td className={`px-2 py-2 bg-amber-50 ${mostrarResultados && index > 0 ? (esCorrecto(index, 'cmg', fila.cmg) ? 'bg-green-100' : 'bg-red-100') : ''}`}>
{index === 0 ? (
<span className="text-gray-400">-</span>
) : (
<div className="flex items-center gap-1">
<span>$</span>
<input
type="number"
value={fila.cmg ?? ''}
onChange={(e) => handleInputChange(index, 'cmg', e.target.value)}
className="w-16 px-1 py-1 border rounded text-sm"
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto(index, 'cmg', fila.cmg) && <CheckCircle className="w-4 h-4 text-green-600" />}
{mostrarResultados && !esCorrecto(index, 'cmg', fila.cmg) && <XCircle className="w-4 h-4 text-red-600" />}
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Button onClick={validar} disabled={!todasCompletadas || mostrarResultados}>
Validar Tabla
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${calcularCorrectas() === totalCampos ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
<Table className="w-5 h-5" />
<span className="font-semibold">Resultado: {calcularCorrectas()}/{totalCampos} campos correctos</span>
</div>
{calcularCorrectas() < totalCampos && (
<p className="text-sm">Revisa tus cálculos. Recuerda: CT = CF + CV, CMe = CT/Q, CMg = CT actual - CT anterior.</p>
)}
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas</h4>
<div className="space-y-1 text-sm text-blue-800">
<p><strong>CT</strong> = CF + CV</p>
<p><strong>CMe</strong> = CT / Q (solo cuando Q {'>'} 0)</p>
<p><strong>CMg</strong> = CTₙ - CTₙ (costo del último trabajador)</p>
</div>
</Card>
</div>
);
}
export default TablaCostos;

View File

@@ -1,3 +1,25 @@
export { FuncionProduccion } from './FuncionProduccion';
export { ProductoTotal } from './ProductoTotal';
export { ProductoMarginal } from './ProductoMarginal';
export { ProductoMedio } from './ProductoMedio';
export { LeyRendimientosDecrecientes } from './LeyRendimientosDecrecientes';
export { EtapasProduccion } from './EtapasProduccion';
export { ProductorRacional } from './ProductorRacional';
export { CortoVsLargoPlazo } from './CortoVsLargoPlazo';
export { CostosFijosVsVariables } from './CostosFijosVsVariables';
export { CostoTotalMedioMarginal } from './CostoTotalMedioMarginal';
export { TablaCostos } from './TablaCostos';
export { CurvasCosto } from './CurvasCosto';
export { CostosMedios } from './CostosMedios';
export { RelacionCMgCMe } from './RelacionCMgCMe';
export { EconomiasEscala } from './EconomiasEscala';
export { DiseconomiasEscala } from './DiseconomiasEscala';
export { CurvaCostoLargoPlazo } from './CurvaCostoLargoPlazo';
export { IngresoTotal } from './IngresoTotal';
export { IngresoMarginal } from './IngresoMarginal';
export { IngresoCompetenciaPerfecta } from './IngresoCompetenciaPerfecta';
export { PuntoCierreEquilibrio } from './PuntoCierreEquilibrio';
export { ReglaImgCmg } from './ReglaImgCmg';
export { CalculadoraCostos } from './CalculadoraCostos';
export { SimuladorProduccion } from './SimuladorProduccion';
export { VisualizadorExcedentes } from './VisualizadorExcedentes';