Files
econ/frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Renato a2ed69fdb8 Fix login blank screen and progress persistence
- Fix authStore to persist user data, not just isAuthenticated
- Fix progressStore handling of undefined API responses
- Remove minimax.md documentation file
- All progress now properly saves to PostgreSQL
- Login flow working correctly
2026-02-12 03:38:33 +01:00

329 lines
12 KiB
TypeScript

import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, RotateCcw, Calculator } from 'lucide-react';
interface FilaCostos {
q: number;
cv: number;
}
interface FilaCalculada extends FilaCostos {
cf: number;
ct: number;
cfme: number;
cvme: number;
cme: number;
cmg: number | null;
}
interface CalculadoraCostosProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function CalculadoraCostos({ ejercicioId: _ejercicioId, onComplete }: CalculadoraCostosProps) {
const CF_BASE = 200;
const [filas, setFilas] = useState<FilaCostos[]>([
{ q: 0, cv: 0 },
{ q: 1, cv: 50 },
{ q: 2, cv: 90 },
{ q: 3, cv: 120 },
{ q: 4, cv: 160 },
{ q: 5, cv: 220 },
{ q: 6, cv: 300 },
{ q: 7, cv: 400 },
{ q: 8, cv: 520 },
]);
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados: FilaCalculada[] = useMemo(() => {
return filas.map((fila, index) => {
const ct = CF_BASE + fila.cv;
const cfme = fila.q > 0 ? CF_BASE / fila.q : 0;
const cvme = fila.q > 0 ? fila.cv / fila.q : 0;
const cme = fila.q > 0 ? ct / fila.q : 0;
const cmg = index > 0 ? ct - (CF_BASE + filas[index - 1].cv) : null;
return {
...fila,
cf: CF_BASE,
ct,
cfme,
cvme,
cme,
cmg,
};
});
}, [filas]);
const handleCvChange = (index: number, valor: string) => {
const numValor = parseFloat(valor) || 0;
const nuevasFilas = [...filas];
nuevasFilas[index] = { ...nuevasFilas[index], cv: numValor };
setFilas(nuevasFilas);
setValidado(false);
};
const validarCalculos = () => {
const nuevosErrores: string[] = [];
datosCalculados.forEach((fila, index) => {
if (fila.ct !== fila.cf + fila.cv) {
nuevosErrores.push(`Fila ${index + 1}: CT no coincide con CF + CV`);
}
if (fila.q > 0 && Math.abs(fila.cme - fila.ct / fila.q) > 0.01) {
nuevosErrores.push(`Fila ${index + 1}: CMe calculado incorrectamente`);
}
});
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0) {
if (onComplete) {
onComplete(100);
}
}
};
const reiniciar = () => {
setFilas([
{ q: 0, cv: 0 },
{ q: 1, cv: 50 },
{ q: 2, cv: 90 },
{ q: 3, cv: 120 },
{ q: 4, cv: 160 },
{ q: 5, cv: 220 },
{ q: 6, cv: 300 },
{ q: 7, cv: 400 },
{ q: 8, cv: 520 },
]);
setValidado(false);
setErrores([]);
};
const maxCT = Math.max(...datosCalculados.map(d => d.ct));
const maxCMe = Math.max(...datosCalculados.filter(d => d.q > 0).map(d => d.cme));
const maxCMg = Math.max(...datosCalculados.filter(d => d.cmg !== null).map(d => d.cmg || 0));
const escalaCT = maxCT > 0 ? 150 / maxCT : 1;
const escalaCMe = maxCMe > 0 ? 150 / maxCMe : 1;
const escalaCMg = maxCMg > 0 ? 150 / maxCMg : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Calculadora de Costos"
subtitle="Ingresa los Costos Variables (CV) y observa los cálculos automáticos"
/>
<div className="overflow-x-auto">
<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">CF</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">CV</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">CFMe</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CVMe</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">CMg</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila, index) => (
<tr key={index} className="border-b hover:bg-gray-50">
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2 text-gray-600">{fila.cf}</td>
<td className="px-3 py-2 bg-blue-50">
<input
type="number"
value={fila.cv}
onChange={(e) => handleCvChange(index, e.target.value)}
className="w-20 px-2 py-1 border rounded text-sm focus:ring-2 focus:ring-primary focus:border-transparent"
min="0"
disabled={fila.q === 0}
/>
</td>
<td className="px-3 py-2 font-medium text-primary">{fila.ct}</td>
<td className="px-3 py-2 text-gray-600">
{fila.q > 0 ? fila.cfme.toFixed(2) : '-'}
</td>
<td className="px-3 py-2 text-gray-600">
{fila.q > 0 ? fila.cvme.toFixed(2) : '-'}
</td>
<td className="px-3 py-2 font-medium text-secondary">
{fila.q > 0 ? fila.cme.toFixed(2) : '-'}
</td>
<td className="px-3 py-2 font-medium text-success">
{fila.cmg !== null ? fila.cmg : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarCalculos} variant="primary">
<Calculator className="w-4 h-4 mr-2" />
Validar Cálculos
</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">¡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">Se encontraron errores:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card>
<CardHeader
title="Visualización de Curvas de Costos"
subtitle="Gráfico de CT, CMe y CMg"
/>
<div className="space-y-6">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Total (CT)</h4>
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
<svg className="w-full h-full" viewBox="0 0 400 160">
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">CT</text>
{datosCalculados.map((d, i) => (
<text key={i} x={30 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="2"
points={datosCalculados.map((d, i) => `${30 + i * 40},${140 - d.ct * escalaCT}`).join(' ')}
/>
{datosCalculados.map((d, i) => (
<circle
key={i}
cx={30 + i * 40}
cy={140 - d.ct * escalaCT}
r="4"
fill="#2563eb"
/>
))}
</svg>
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Medio (CMe) vs Costo Marginal (CMg)</h4>
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
<svg className="w-full h-full" viewBox="0 0 400 160">
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">Costo</text>
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
<text key={i} x={70 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="2"
points={datosCalculados
.filter(d => d.q > 0)
.map((d, i) => `${70 + i * 40},${140 - d.cme * escalaCMe}`)
.join(' ')}
/>
<polyline
fill="none"
stroke="#16a34a"
strokeWidth="2"
strokeDasharray="4"
points={datosCalculados
.filter(d => d.cmg !== null)
.map((d, i) => `${70 + i * 40},${140 - (d.cmg || 0) * escalaCMg}`)
.join(' ')}
/>
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
<circle
key={`cme-${i}`}
cx={70 + i * 40}
cy={140 - d.cme * escalaCMe}
r="4"
fill="#7c3aed"
/>
))}
{datosCalculados.filter(d => d.cmg !== null).map((d, i) => (
<circle
key={`cmg-${i}`}
cx={70 + i * 40}
cy={140 - (d.cmg || 0) * escalaCMg}
r="4"
fill="#16a34a"
/>
))}
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#7c3aed" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">CMe</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#16a34a" strokeWidth="2" strokeDasharray="4" />
<text x="25" y="19" className="text-xs fill-gray-600">CMg</text>
</g>
</svg>
</div>
</div>
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas utilizadas:</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li><strong>CT</strong> = CF + CV (Costo Total)</li>
<li><strong>CFMe</strong> = CF / Q (Costo Fijo Medio)</li>
<li><strong>CVMe</strong> = CV / Q (Costo Variable Medio)</li>
<li><strong>CMe</strong> = CT / Q (Costo Medio)</li>
<li><strong>CMg</strong> = ΔCT / ΔQ (Costo Marginal)</li>
</ul>
</Card>
</div>
);
}
export default CalculadoraCostos;