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