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:
213
frontend/src/components/exercises/modulo4/CortoVsLargoPlazo.tsx
Normal file
213
frontend/src/components/exercises/modulo4/CortoVsLargoPlazo.tsx
Normal 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;
|
||||
@@ -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(2→3)=$40, CMg(4→5)=$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;
|
||||
@@ -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;
|
||||
204
frontend/src/components/exercises/modulo4/CostosMedios.tsx
Normal file
204
frontend/src/components/exercises/modulo4/CostosMedios.tsx
Normal 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;
|
||||
@@ -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;
|
||||
218
frontend/src/components/exercises/modulo4/CurvasCosto.tsx
Normal file
218
frontend/src/components/exercises/modulo4/CurvasCosto.tsx
Normal 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;
|
||||
309
frontend/src/components/exercises/modulo4/DiseconomiasEscala.tsx
Normal file
309
frontend/src/components/exercises/modulo4/DiseconomiasEscala.tsx
Normal 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;
|
||||
217
frontend/src/components/exercises/modulo4/EconomiasEscala.tsx
Normal file
217
frontend/src/components/exercises/modulo4/EconomiasEscala.tsx
Normal 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;
|
||||
231
frontend/src/components/exercises/modulo4/EtapasProduccion.tsx
Normal file
231
frontend/src/components/exercises/modulo4/EtapasProduccion.tsx
Normal 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;
|
||||
184
frontend/src/components/exercises/modulo4/FuncionProduccion.tsx
Normal file
184
frontend/src/components/exercises/modulo4/FuncionProduccion.tsx
Normal 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;
|
||||
@@ -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;
|
||||
234
frontend/src/components/exercises/modulo4/IngresoMarginal.tsx
Normal file
234
frontend/src/components/exercises/modulo4/IngresoMarginal.tsx
Normal 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;
|
||||
273
frontend/src/components/exercises/modulo4/IngresoTotal.tsx
Normal file
273
frontend/src/components/exercises/modulo4/IngresoTotal.tsx
Normal 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;
|
||||
@@ -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 (1→2) = (100-50)/(2-1) = 50 Tn<br />
|
||||
PMg (4→5) = (160-155)/(5-4) = 5 Tn
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LeyRendimientosDecrecientes;
|
||||
233
frontend/src/components/exercises/modulo4/ProductoMarginal.tsx
Normal file
233
frontend/src/components/exercises/modulo4/ProductoMarginal.tsx
Normal 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;
|
||||
247
frontend/src/components/exercises/modulo4/ProductoMedio.tsx
Normal file
247
frontend/src/components/exercises/modulo4/ProductoMedio.tsx
Normal 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;
|
||||
223
frontend/src/components/exercises/modulo4/ProductoTotal.tsx
Normal file
223
frontend/src/components/exercises/modulo4/ProductoTotal.tsx
Normal 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;
|
||||
199
frontend/src/components/exercises/modulo4/ProductorRacional.tsx
Normal file
199
frontend/src/components/exercises/modulo4/ProductorRacional.tsx
Normal 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;
|
||||
@@ -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;
|
||||
309
frontend/src/components/exercises/modulo4/ReglaImgCmg.tsx
Normal file
309
frontend/src/components/exercises/modulo4/ReglaImgCmg.tsx
Normal 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;
|
||||
235
frontend/src/components/exercises/modulo4/RelacionCMgCMe.tsx
Normal file
235
frontend/src/components/exercises/modulo4/RelacionCMgCMe.tsx
Normal 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;
|
||||
200
frontend/src/components/exercises/modulo4/TablaCostos.tsx
Normal file
200
frontend/src/components/exercises/modulo4/TablaCostos.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user