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
This commit is contained in:
328
frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Normal file
328
frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user